tirtc-device-builder 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """Inspect the ESP-IDF application descriptor in an application BIN."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import struct
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ ESP_IMAGE_MAGIC = 0xE9
16
+ ESP_APP_DESC_MAGIC = 0xABCD5432
17
+ IMAGE_HEADER_SIZE = 24
18
+ SEGMENT_HEADER_SIZE = 8
19
+ APP_DESC_OFFSET = IMAGE_HEADER_SIZE + SEGMENT_HEADER_SIZE
20
+ APP_DESC_REQUIRED_SIZE = 176
21
+
22
+
23
+ def sha256_file(path: Path) -> str:
24
+ digest = hashlib.sha256()
25
+ with path.open("rb") as stream:
26
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
27
+ digest.update(chunk)
28
+ return digest.hexdigest()
29
+
30
+
31
+ def descriptor_string(data: bytes, offset: int, size: int, label: str) -> str:
32
+ raw = data[offset : offset + size]
33
+ value = raw.split(b"\0", 1)[0]
34
+ try:
35
+ decoded = value.decode("utf-8")
36
+ except UnicodeDecodeError as exc:
37
+ raise ValueError(f"application descriptor {label} is not UTF-8") from exc
38
+ if any(ord(character) < 0x20 or ord(character) == 0x7F for character in decoded):
39
+ raise ValueError(f"application descriptor {label} contains control bytes")
40
+ return decoded
41
+
42
+
43
+ def inspect_firmware(
44
+ firmware_path: Path,
45
+ *,
46
+ elf_path: Path | None = None,
47
+ expected_version: str | None = None,
48
+ ) -> dict[str, Any]:
49
+ firmware = firmware_path.expanduser().resolve()
50
+ try:
51
+ data = firmware.read_bytes()
52
+ except OSError as exc:
53
+ raise ValueError(f"cannot read application BIN {firmware}: {exc}") from exc
54
+
55
+ minimum_size = APP_DESC_OFFSET + APP_DESC_REQUIRED_SIZE
56
+ if len(data) < minimum_size:
57
+ raise ValueError(
58
+ f"application BIN is truncated: need at least {minimum_size} bytes"
59
+ )
60
+ if data[0] != ESP_IMAGE_MAGIC:
61
+ raise ValueError(
62
+ f"not an ESP application image: expected magic 0x{ESP_IMAGE_MAGIC:02x}"
63
+ )
64
+
65
+ segment_length = struct.unpack_from("<I", data, IMAGE_HEADER_SIZE + 4)[0]
66
+ if segment_length < APP_DESC_REQUIRED_SIZE:
67
+ raise ValueError("first ESP image segment is too small for esp_app_desc_t")
68
+
69
+ descriptor = data[APP_DESC_OFFSET : APP_DESC_OFFSET + APP_DESC_REQUIRED_SIZE]
70
+ descriptor_magic = struct.unpack_from("<I", descriptor, 0)[0]
71
+ if descriptor_magic != ESP_APP_DESC_MAGIC:
72
+ raise ValueError(
73
+ "first ESP image segment does not start with a valid esp_app_desc_t"
74
+ )
75
+
76
+ version = descriptor_string(descriptor, 16, 32, "version")
77
+ if not version:
78
+ raise ValueError("application descriptor version is empty")
79
+ if expected_version is not None and version != expected_version:
80
+ raise ValueError(
81
+ f"firmware version mismatch: expected {expected_version!r}, got {version!r}"
82
+ )
83
+
84
+ elf_digest = descriptor[144:176].hex()
85
+ result: dict[str, Any] = {
86
+ "firmware_path": str(firmware),
87
+ "size_bytes": len(data),
88
+ "bin_sha256": hashlib.sha256(data).hexdigest(),
89
+ "segment_count": data[1],
90
+ "project_name": descriptor_string(descriptor, 48, 32, "project_name"),
91
+ "version": version,
92
+ "build_time": descriptor_string(descriptor, 80, 16, "time"),
93
+ "build_date": descriptor_string(descriptor, 96, 16, "date"),
94
+ "idf_version": descriptor_string(descriptor, 112, 32, "idf_ver"),
95
+ "app_elf_sha256": elf_digest,
96
+ }
97
+
98
+ if elf_path is not None:
99
+ elf = elf_path.expanduser().resolve()
100
+ try:
101
+ elf_size = elf.stat().st_size
102
+ actual_elf_digest = sha256_file(elf)
103
+ except OSError as exc:
104
+ raise ValueError(f"cannot read ELF {elf}: {exc}") from exc
105
+ if actual_elf_digest != elf_digest:
106
+ raise ValueError(
107
+ "ELF SHA-256 mismatch: application descriptor records "
108
+ f"{elf_digest}, file is {actual_elf_digest}"
109
+ )
110
+ result.update(
111
+ {
112
+ "elf_path": str(elf),
113
+ "elf_size_bytes": elf_size,
114
+ "elf_sha256": actual_elf_digest,
115
+ }
116
+ )
117
+
118
+ return result
119
+
120
+
121
+ def print_human(result: dict[str, Any]) -> None:
122
+ fields = (
123
+ ("firmware", "firmware_path"),
124
+ ("project", "project_name"),
125
+ ("version", "version"),
126
+ ("build", "build_date"),
127
+ ("time", "build_time"),
128
+ ("idf", "idf_version"),
129
+ ("size_bytes", "size_bytes"),
130
+ ("bin_sha256", "bin_sha256"),
131
+ ("app_elf_sha256", "app_elf_sha256"),
132
+ ("elf", "elf_path"),
133
+ ("elf_size_bytes", "elf_size_bytes"),
134
+ ("elf_sha256", "elf_sha256"),
135
+ )
136
+ for label, key in fields:
137
+ if key in result:
138
+ print(f"{label}: {result[key]}")
139
+
140
+
141
+ def build_parser() -> argparse.ArgumentParser:
142
+ parser = argparse.ArgumentParser(
143
+ description=(
144
+ "inspect version and SHA-256 identity from an ESP-IDF application BIN"
145
+ )
146
+ )
147
+ parser.add_argument("firmware", type=Path, help="ESP-IDF application BIN")
148
+ parser.add_argument(
149
+ "--elf",
150
+ type=Path,
151
+ help="matching ELF; its SHA-256 must equal app_elf_sha256 in the BIN",
152
+ )
153
+ parser.add_argument(
154
+ "--expect-version",
155
+ help="fail unless the application descriptor contains this exact version",
156
+ )
157
+ parser.add_argument("--json", action="store_true", help="emit JSON")
158
+ return parser
159
+
160
+
161
+ def main(argv: list[str] | None = None) -> int:
162
+ args = build_parser().parse_args(argv)
163
+ try:
164
+ result = inspect_firmware(
165
+ args.firmware,
166
+ elf_path=args.elf,
167
+ expected_version=args.expect_version,
168
+ )
169
+ except ValueError as exc:
170
+ print(f"error: {exc}", file=sys.stderr)
171
+ return 2
172
+ if args.json:
173
+ print(json.dumps(result, indent=2, sort_keys=True))
174
+ else:
175
+ print_human(result)
176
+ return 0
177
+
178
+
179
+ if __name__ == "__main__":
180
+ raise SystemExit(main())