From 60f926176bd780c74950c8915e1d67ab3252a09b Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:47:19 -0700 Subject: [PATCH] Reject malformed SMPTE time codes The drop-frame separator alternation '(:|;|.|,)' contained an unescaped '.', which in a regular expression matches any character, so any separator was accepted (e.g. '01x02x03x04'). Parsing also used re.match, which is not anchored at the end of the string, so trailing garbage such as '01:02:03:04garbage' was silently ignored. Use an explicit [:;.,] character class and fullmatch. The same pattern in the SCC line parser is fixed identically. Adds a regression test covering wildcard separators and trailing garbage. --- src/main/python/ttconv/scc/line.py | 2 +- src/main/python/ttconv/time_code.py | 6 +++--- src/test/python/test_time_code_smpte.py | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/python/ttconv/scc/line.py b/src/main/python/ttconv/scc/line.py index cc19da43..33cd5384 100644 --- a/src/main/python/ttconv/scc/line.py +++ b/src/main/python/ttconv/scc/line.py @@ -50,7 +50,7 @@ '(?P[0-9]{2})', '(?P[0-9]{2})', '(?P[0-9]{2})']) -DF_PATTERN = '(:|;|.|,)'.join(['(?P[0-9]{2})', +DF_PATTERN = '[:;.,]'.join(['(?P[0-9]{2})', '(?P[0-9]{2})', '(?P[0-9]{2})', '(?P[0-9]{2})']) diff --git a/src/main/python/ttconv/time_code.py b/src/main/python/ttconv/time_code.py index 4670e122..c5cf03f5 100644 --- a/src/main/python/ttconv/time_code.py +++ b/src/main/python/ttconv/time_code.py @@ -145,7 +145,7 @@ class SmpteTimeCode(_HHMMSSTimeExpression): _NDF_TC_RE = re.compile(_NDF_TC_PATTERN) - _DF_TC_PATTERN = '(:|;|.|,)'.join(['(?P[0-9]{2})', + _DF_TC_PATTERN = '[:;.,]'.join(['(?P[0-9]{2})', '(?P[0-9]{2})', '(?P[0-9]{2})', '(?P[0-9]{2})']) @@ -222,7 +222,7 @@ def add_frames(self, nb_frames=1): def parse(time_code: str, frame_rate: Fraction) -> SmpteTimeCode: """Reads the time code string and converts to a SmpteTimeCode instance""" - match = SmpteTimeCode._NDF_TC_RE.match(time_code) + match = SmpteTimeCode._NDF_TC_RE.fullmatch(time_code) if match is not None: # NDF timecode @@ -236,7 +236,7 @@ def parse(time_code: str, frame_rate: Fraction) -> SmpteTimeCode: if frame_rate.denominator != 1001: frame_rate = frame_rate * Fraction(1000, 1001) - match = SmpteTimeCode.DF_TC_RE.match(time_code) + match = SmpteTimeCode.DF_TC_RE.fullmatch(time_code) if match is not None: return SmpteTimeCode(int(match.group('df_h')), diff --git a/src/test/python/test_time_code_smpte.py b/src/test/python/test_time_code_smpte.py index 9fab10d2..d53e4b5d 100644 --- a/src/test/python/test_time_code_smpte.py +++ b/src/test/python/test_time_code_smpte.py @@ -107,6 +107,14 @@ def test_parse_drop_frame_time_code(self): self.assertEqual(Fraction(111582, Fraction(30000, 1001)), time_code.to_temporal_offset()) self.assertEqual("01:02:03;04", str(time_code)) + def test_parse_rejects_invalid_time_code(self): + # the separator alternation used to include an unescaped '.', which matches + # any character, and matching was not anchored to the end of the string + for time_code in ["01x02x03x04", "01 02-03/04", "01:02:03:04garbage", "01:02:03.456"]: + with self.subTest(time_code=time_code): + with self.assertRaises(ValueError): + SmpteTimeCode.parse(time_code, FPS_29_97) + def test_time_code_frames_conversion(self): time_code = SmpteTimeCode.from_frames(1795, FPS_30) self.assertEqual("00:00:59:25", str(time_code))