Skip to content

Commit 49cdc4e

Browse files
gijzelaerrclaude
andcommitted
Fix get_dtl to read full 4-byte nanosecond field and clean up fuzz test
get_dtl was reading only byte 8 as a raw integer for microseconds, but the DTL format stores nanoseconds as a 4-byte big-endian uint32 in bytes 8-11. This fix reads the full field and converts ns to us. Also removes unnecessary KeyError catch from PDU fuzz test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 86c59d2 commit 49cdc4e

2 files changed

Lines changed: 29 additions & 18 deletions

File tree

snap7/util/getters.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,15 +656,38 @@ def get_ldt(bytearray_: bytearray, byte_index: int) -> datetime:
656656

657657

658658
def get_dtl(bytearray_: bytearray, byte_index: int) -> datetime:
659+
"""Get DTL (Date and Time Long) value from bytearray.
660+
661+
Notes:
662+
Datatype ``DTL`` consists of 12 bytes in the PLC:
663+
- Bytes 0-1: Year (uint16, big-endian)
664+
- Byte 2: Month (1-12)
665+
- Byte 3: Day (1-31)
666+
- Byte 4: Weekday (1=Sunday, 7=Saturday)
667+
- Byte 5: Hour (0-23)
668+
- Byte 6: Minute (0-59)
669+
- Byte 7: Second (0-59)
670+
- Bytes 8-11: Nanoseconds (uint32, big-endian)
671+
672+
Args:
673+
bytearray_: buffer to read from.
674+
byte_index: byte index from where to start reading.
675+
676+
Returns:
677+
datetime value (microsecond precision; sub-microsecond nanoseconds are truncated).
678+
"""
679+
nanoseconds = struct.unpack(">I", bytearray_[byte_index + 8 : byte_index + 12])[0]
680+
microsecond = nanoseconds // 1000
681+
659682
time_to_datetime = datetime(
660683
year=int.from_bytes(bytearray_[byte_index : byte_index + 2], byteorder="big"),
661684
month=int(bytearray_[byte_index + 2]),
662685
day=int(bytearray_[byte_index + 3]),
663686
hour=int(bytearray_[byte_index + 5]),
664687
minute=int(bytearray_[byte_index + 6]),
665688
second=int(bytearray_[byte_index + 7]),
666-
microsecond=int(bytearray_[byte_index + 8]),
667-
) # --- ? noch nicht genau genug
689+
microsecond=microsecond,
690+
)
668691
if time_to_datetime > datetime(2554, 12, 31, 23, 59, 59):
669692
raise ValueError("date_val is higher than specification allows.")
670693
return time_to_datetime

tests/test_hypothesis.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -302,24 +302,12 @@ def test_dt_roundtrip(value: datetime) -> None:
302302
)
303303
)
304304
def test_dtl_roundtrip(value: datetime) -> None:
305-
# DTL stores nanoseconds derived from microseconds, but get_dtl reads
306-
# byte 8 as raw microsecond value. Truncate to match.
307-
# set_dtl writes microsecond * 1000 as nanoseconds (4 bytes big-endian)
308-
# get_dtl reads byte 8 as int — this is the first byte of the 4-byte nanosecond field
309-
nanoseconds = value.microsecond * 1000
310-
ns_bytes = struct.pack(">I", nanoseconds)
311-
expected_microsecond = ns_bytes[0] # get_dtl reads only byte_index+8
312-
305+
# DTL stores nanoseconds (microsecond * 1000), so the roundtrip
306+
# preserves microsecond precision exactly.
313307
data = bytearray(12)
314308
set_dtl(data, 0, value)
315309
result = get_dtl(data, 0)
316-
assert result.year == value.year
317-
assert result.month == value.month
318-
assert result.day == value.day
319-
assert result.hour == value.hour
320-
assert result.minute == value.minute
321-
assert result.second == value.second
322-
assert result.microsecond == expected_microsecond
310+
assert result == value
323311

324312

325313
# ---------------------------------------------------------------------------
@@ -530,7 +518,7 @@ def test_pdu_parse_no_crash(data: bytes) -> None:
530518
proto = S7Protocol()
531519
try:
532520
proto.parse_response(data)
533-
except (S7ProtocolError, struct.error, ValueError, IndexError, KeyError):
521+
except (S7ProtocolError, struct.error, ValueError, IndexError):
534522
pass # Expected for malformed data
535523

536524

0 commit comments

Comments
 (0)