Validate ARC timestamps and reject bad responses

Add strict validation and error handling for ARC timestamps: require exactly 8 bytes and raise a ValueError for out-of-range timestamps (wraps OverflowError/OSError/ValueError from datetime). parse_arc_response now catches timestamp decode failures and returns an empty list for invalid ARC responses. Added a unit test that injects invalid timestamp noise to ensure the parser rejects such responses.
This commit is contained in:
Golumpa 2026-06-12 10:32:17 +01:00
parent 0c9026284c
commit 1144e33a57
2 changed files with 20 additions and 2 deletions

View file

@ -50,9 +50,14 @@ def decode_arc_key(raw: bytes) -> str | None:
def decode_arc_timestamp(raw8: bytes) -> tuple[int, float, str]:
if len(raw8) != 8:
raise ValueError("arc timestamps must be exactly 8 bytes")
ticks = struct.unpack("<Q", raw8)[0]
unix_seconds = ticks / ARC_TIMESTAMP_TICKS_PER_SECOND - DOTNET_UNIX_EPOCH_SECONDS
decoded = datetime.fromtimestamp(unix_seconds, timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
try:
decoded = datetime.fromtimestamp(unix_seconds, timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
except (OverflowError, OSError, ValueError) as exc:
raise ValueError("arc timestamp is out of range") from exc
return ticks, unix_seconds, decoded
@ -85,7 +90,10 @@ def parse_arc_response(response: bytes) -> list[dict[str, Any]]:
pos += 8
arc_id = decode_arc_key(name_raw) or name_raw.hex()
meta = ARC_META.get(arc_id, {})
ticks, unix_seconds, timestamp_decoded = decode_arc_timestamp(timestamp_raw)
try:
ticks, unix_seconds, timestamp_decoded = decode_arc_timestamp(timestamp_raw)
except ValueError:
return []
records.append(
{
"record_start": start,

View file

@ -356,6 +356,16 @@ class BoundaryExportTests(unittest.TestCase):
self.assertEqual(decoded[0]["reward_type"], "arc")
self.assertEqual(decoded[0]["reward_key_hex"], reference_rows[0]["arc_key_hex"])
def test_arc_response_parser_rejects_invalid_timestamp_noise(self):
response = bytearray(0x4C)
response += (10).to_bytes(4, "little")
response += bytes.fromhex("ccdee4d6be")
response += (8).to_bytes(4, "little")
response += b"garb"
response += (0xFFFFFFFFFFFFFFFF).to_bytes(8, "little")
self.assertEqual(parse_arc_response(bytes(response)), [])
def test_arc_partial_timestamp_group_is_exported_without_warning(self):
rows = load_arc_csv("arc_pages_1_to_5_v2.csv")
pairs = []