A Python port of the Invisible Internet Project (I2P)
1"""SU3 file format parser — signed update file format.
2
3Parses the header of I2P's SU3 update files which contain
4signed router updates, plugins, and other content.
5
6Ported from net.i2p.crypto.SU3File.
7"""
8
9from __future__ import annotations
10
11import struct
12from dataclasses import dataclass
13
14_SU3_MAGIC = b"I2Psu3"
15_MIN_HEADER_SIZE = 40
16
17
18@dataclass
19class SU3Header:
20 """Parsed SU3 file header."""
21 magic: str
22 format_version: int
23 sig_type: int
24 sig_length: int
25 content_type: int
26 file_type: int
27 version_length: int
28 signer_length: int
29 content_length: int
30
31
32def parse_su3_header(data: bytes) -> SU3Header | None:
33 """Parse an SU3 file header from raw bytes.
34
35 Returns None if the data is too short or has invalid magic.
36 """
37 if len(data) < _MIN_HEADER_SIZE:
38 return None
39
40 # Check magic (first 6 bytes)
41 if data[:6] != _SU3_MAGIC:
42 return None
43
44 # Parse header fields
45 # Byte 6: unused
46 # Byte 7: format version (currently 0)
47 format_version = data[7]
48
49 # Bytes 8-9: signature type (big-endian uint16)
50 sig_type = struct.unpack(">H", data[8:10])[0]
51
52 # Bytes 10-11: signature length
53 sig_length = struct.unpack(">H", data[10:12])[0]
54
55 # Byte 12: unused
56 # Byte 13: content type
57 content_type = data[13] if len(data) > 13 else 0
58
59 # Byte 14: unused
60 # Byte 15: file type
61 file_type = data[15] if len(data) > 15 else 0
62
63 # Byte 16: version string length
64 version_length = data[16] if len(data) > 16 else 0
65
66 # Byte 17: unused
67 # Byte 18: signer ID length
68 signer_length = data[18] if len(data) > 18 else 0
69
70 # Bytes 20-27: content length (big-endian uint64)
71 content_length = 0
72 if len(data) >= 28:
73 content_length = struct.unpack(">Q", data[20:28])[0]
74
75 return SU3Header(
76 magic=_SU3_MAGIC.decode("ascii"),
77 format_version=format_version,
78 sig_type=sig_type,
79 sig_length=sig_length,
80 content_type=content_type,
81 file_type=file_type,
82 version_length=version_length,
83 signer_length=signer_length,
84 content_length=content_length,
85 )