Skip to content

Commit 27d9451

Browse files
Fix date strings in ban reason parsed as duration (#41)
getTs treated a token as a duration whenever a duration substring appeared anywhere inside it. A reason word like 'username5d' was read as 5 days, added to the expiry, and dropped from the stored reason. Require the whole token to be duration units (re.fullmatch) so only genuine duration words (4w, 2h, 1w2d3h) are consumed; any other word falls through to the reason via rest('text').
1 parent ffe0cbd commit 27d9451

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

plugin.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,8 +1406,13 @@ def isRe(s):
14061406

14071407
# Taken from plugins.Time.seconds
14081408
_SECONDS_RE = re.compile(r'-?[0-9]+[ywdhms]')
1409+
# A duration argument must consist *only* of duration tokens, so a reason
1410+
# word like "username5d" is not mistaken for "5 days" (issue #41).
1411+
_DURATION_RE = re.compile(r'(?:-?[0-9]+[ywdhms])+')
14091412
def getTs(irc, msg, args, state):
14101413
seconds = None
1414+
if not args or _DURATION_RE.fullmatch(args[0]) is None:
1415+
raise callbacks.ArgumentError
14111416
secs = _SECONDS_RE.findall(args[0])
14121417
for sec in secs:
14131418
(i, kind) = int(sec[:-1]), sec[-1]

test.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from supybot.test import *
3131

3232
import time
33+
import supybot.callbacks as callbacks
3334
import supybot.conf as conf
3435
import supybot.ircmsgs as ircmsgs
3536

@@ -229,6 +230,40 @@ def testGetDuration(self):
229230
self.assertEqual(plugin.getDuration([60, 30, 10]), 100)
230231
self.assertEqual(plugin.getDuration([3600]), 3600)
231232

233+
def testGetTs(self):
234+
# getTs is the duration converter: it reads/mutates the args list and
235+
# appends to state.args; its irc/msg params are unused (issue #41)
236+
class _State:
237+
def __init__(self):
238+
self.args = []
239+
# a bare duration word parses and is consumed
240+
st = _State(); args = ['4w']
241+
plugin.getTs(None, None, args, st)
242+
self.assertEqual(st.args, [4 * 604800.0])
243+
self.assertEqual(args, [], 'a consumed duration word is removed')
244+
# a compound word "1w2d3h" sums its parts
245+
st = _State(); args = ['1w2d3h']
246+
plugin.getTs(None, None, args, st)
247+
self.assertEqual(st.args, [float(604800 + 2 * 86400 + 3 * 3600)])
248+
# a multi-word duration "4d 2h ...": getTs is called once per word,
249+
# each whole word is a valid duration, so both are accepted
250+
st = _State(); args = ['4d', '2h', 'ban', 'evading']
251+
plugin.getTs(None, None, args, st)
252+
plugin.getTs(None, None, args, st)
253+
self.assertEqual(st.args, [4 * 86400.0, 2 * 3600.0])
254+
self.assertEqual(args, ['ban', 'evading'],
255+
'words after the duration are left for the reason')
256+
# issue #41: a date string inside a word is NOT a duration
257+
st = _State(); args = ['username5d', 'ban', 'evading']
258+
self.assertRaises(callbacks.ArgumentError,
259+
plugin.getTs, None, None, args, st)
260+
self.assertEqual(args, ['username5d', 'ban', 'evading'],
261+
'a rejected word is left in place for the reason')
262+
# a plain word is rejected
263+
st = _State(); args = ['evading']
264+
self.assertRaises(callbacks.ArgumentError,
265+
plugin.getTs, None, None, args, st)
266+
232267
def testClearExtendedBanPattern(self):
233268
# extban support is read from irc.state.supported (prefix,modes)
234269
self.irc.state.supported['extban'] = '$,ajrxz'

0 commit comments

Comments
 (0)