From 3fc237f466c7ff2cf5e67e599bbb796ef105fff8 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 24 Apr 2025 18:30:53 +0200 Subject: [PATCH 1/2] Additional invalid AM timecode tests (failing) --- test/testwwvb.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/testwwvb.py b/test/testwwvb.py index 3a19c38..db981e8 100755 --- a/test/testwwvb.py +++ b/test/testwwvb.py @@ -395,6 +395,38 @@ class WWVBRoundtrip(unittest.TestCase): self.assertEqual(WWVBMinute2k(2070, 1, 1, 0, 0).year, 2070) self.assertEqual(WWVBMinute2k(2099, 1, 1, 0, 0).year, 2099) + def test_invalid_minute(self) -> None: + """Check that minute 61 is not valid in an AM timecode""" + base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0) + minute = base_minute.as_timecode() + minute._put_am_bcd(61, 1, 2, 3, 5, 6, 7, 8) # valid BCD, invalid minute + decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute) + assert decoded_minute is None + + def test_invalid_hour(self) -> None: + """Check that hour 25 is not valid in an AM timecode""" + base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0) + minute = base_minute.as_timecode() + minute._put_am_bcd(29, 12, 13, 15, 16, 17, 18) # valid BCD, invalid hour + decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute) + assert decoded_minute is None + + def test_invalid_bcd_day(self) -> None: + """Check that invalid BCD is detected in AM timecode""" + base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0) + minute = base_minute.as_timecode() + minute.am[30:34] = [wwvb.AmplitudeModulation.ONE] * 4 # invalid BCD 0xf + decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute) + assert decoded_minute is None + + def test_invalid_mark(self) -> None: + """Check that invalid presence of MARK in a data field is detected""" + base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0) + minute = base_minute.as_timecode() + minute.am[57] = wwvb.AmplitudeModulation.MARK + decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute) + assert decoded_minute is None + if __name__ == "__main__": unittest.main() From ec68ed225f24c298828d6bb239cb76de952d582e Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 24 Apr 2025 18:31:34 +0200 Subject: [PATCH 2/2] Additional error checking in from_timecode_am the change from using reversed() is because the pos sequence is now traversed twice. --- src/wwvb/__init__.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/wwvb/__init__.py b/src/wwvb/__init__.py index f9f0c48..314aa88 100644 --- a/src/wwvb/__init__.py +++ b/src/wwvb/__init__.py @@ -24,12 +24,6 @@ SECOND = datetime.timedelta(seconds=1) T = TypeVar("T") -def _require(x: T | None) -> T: - """Check an Optional item is not None.""" - assert x is not None - return x - - def _removeprefix(s: str, p: str) -> str: if s.startswith(p): return s[len(p) :] @@ -689,7 +683,7 @@ class WWVBMinute(_WWVBMinute): return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls) @classmethod - def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None: + def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None: # noqa: PLR0912 """Construct a WWVBMinute from a WWVBTimecode""" for i in (0, 9, 19, 29, 39, 49, 59): if t.am[i] != AmplitudeModulation.MARK: @@ -704,9 +698,13 @@ class WWVBMinute(_WWVBMinute): minute = t._get_am_bcd(1, 2, 3, 5, 6, 7, 8) if minute is None: return None + if minute >= 60: + return None hour = t._get_am_bcd(12, 13, 15, 16, 17, 18) if hour is None: return None + if hour >= 24: + return None days = t._get_am_bcd(22, 23, 25, 26, 27, 28, 30, 31, 32, 33) if days is None: return None @@ -723,7 +721,9 @@ class WWVBMinute(_WWVBMinute): if days > 366 or (not ly and days > 365): return None ls = bool(t.am[56]) - dst = _require(t._get_am_bcd(57, 58)) + dst = t._get_am_bcd(57, 58) + if dst is None: + return None return cls(year, days, hour, minute, dst, ut1, ls, ly) @@ -784,7 +784,10 @@ class WWVBTimecode: The the bits ``self.am[poslist[i]]`` in MSB order are converted from BCD to integer """ - pos = reversed(poslist) + pos = list(poslist)[::-1] + for p in pos: + if self.am[p] not in {AmplitudeModulation.ZERO, AmplitudeModulation.ONE}: + return None val = [bool(self.am[p]) for p in pos] result = 0 base = 1