tan-cli 0.7.0__py3-none-any.whl
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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/model/measure.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Host reference run + A/B compare (tan-cli#1287, host tier only).
|
|
3
|
+
|
|
4
|
+
Ported from alp-sdk `integration/model-edge-ai` (`scripts/alp_model/measure.py`,
|
|
5
|
+
alp-sdk#933). The host run (`backend="cpu-host"`) is a FUNCTIONAL + host-latency
|
|
6
|
+
reference, NOT the target SoM's performance. The on-device tier (target
|
|
7
|
+
latency, peak arena SRAM, per-rail energy via the on-board monitor IC) is the
|
|
8
|
+
bench-gated follow-on that fills the SAME `RunResult` schema with real numbers;
|
|
9
|
+
the host run leaves `power_mj`/`peak_sram_kib` `None` and nothing here
|
|
10
|
+
fabricates them; the device tier (`tan.core.model_device`) fills `peak_sram_kib`
|
|
11
|
+
from the app's ENERGY-CFG `sram_peak_bytes` and energy from its capture. The energy-integration math (`integrate_energy`, `windowed_delta`,
|
|
12
|
+
`EnergyMeasurement`) is the on-device tier's, ported unchanged and PURE; the
|
|
13
|
+
estimate-vs-measured feedback loop is not ported yet.
|
|
14
|
+
|
|
15
|
+
`numpy`/`onnxruntime` (the optional `model` extra) are imported inside the
|
|
16
|
+
functions that use them.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from statistics import median
|
|
23
|
+
from time import perf_counter
|
|
24
|
+
from typing import Any, Sequence
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MeasureError(Exception):
|
|
28
|
+
"""The model could not be loaded or run for measurement."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# (t_seconds, power_watts) — power_watts is an on-board monitor Power-register (3h) read.
|
|
32
|
+
PowerSample = tuple[float, float]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def integrate_energy(samples: Sequence[PowerSample]) -> float:
|
|
36
|
+
"""Trapezoidal time-integration of power samples into millijoules:
|
|
37
|
+
E = Σ 0.5*(P_i + P_i+1)*Δt over consecutive samples (Δt in seconds,
|
|
38
|
+
P in watts -> joules, *1000 -> mJ). Samples must be in ascending t order
|
|
39
|
+
(the on-board monitor has no on-chip energy accumulator, so this is the whole
|
|
40
|
+
point of taking repeated Power-register samples host-side).
|
|
41
|
+
<2 samples -> 0.0 (no interval to integrate; not an error — a single
|
|
42
|
+
reading carries no energy information, so returning 0.0 lets a caller
|
|
43
|
+
that hasn't collected a real window yet compose without special-casing)."""
|
|
44
|
+
if len(samples) < 2:
|
|
45
|
+
return 0.0
|
|
46
|
+
energy_j = 0.0
|
|
47
|
+
for (t0, p0), (t1, p1) in zip(samples, samples[1:]):
|
|
48
|
+
energy_j += 0.5 * (p0 + p1) * (t1 - t0)
|
|
49
|
+
return energy_j * 1000.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def windowed_delta(active_samples: Sequence[PowerSample],
|
|
53
|
+
idle_samples: Sequence[PowerSample], n_inferences: int) -> float:
|
|
54
|
+
"""Idle-baseline-subtracted energy per inference, in mJ: the delta between
|
|
55
|
+
an active (inferring) window and an idle window, divided by how many
|
|
56
|
+
inferences ran in the active window.
|
|
57
|
+
|
|
58
|
+
Precondition: active_samples and idle_samples MUST cover equal wall-clock
|
|
59
|
+
duration (same integration window length). This is a subtraction of two
|
|
60
|
+
integrals, not a rate comparison -- unequal durations bias the baseline
|
|
61
|
+
(e.g. a longer idle window subtracts more idle energy than the active
|
|
62
|
+
window actually contains). The caller/firmware is responsible for taking
|
|
63
|
+
symmetric windows; this function does not (and should not) assert it,
|
|
64
|
+
since real hardware timing has jitter that would make a hard equality
|
|
65
|
+
check false-trip."""
|
|
66
|
+
if n_inferences < 1:
|
|
67
|
+
raise ValueError(f"n_inferences must be >= 1, got {n_inferences}")
|
|
68
|
+
return (integrate_energy(active_samples) - integrate_energy(idle_samples)) / n_inferences
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class EnergyMeasurement:
|
|
73
|
+
"""A real bench-measured energy result — never fabricate one; `run_host()`
|
|
74
|
+
leaves `RunResult.energy` as None instead. `source`/`scope` default to (and
|
|
75
|
+
are validated as) the honest labels: this is a board-level carrier-rail
|
|
76
|
+
delta, NEVER an isolated NPU/U85/U55/M55 silicon energy figure."""
|
|
77
|
+
value_mj_per_inference: float
|
|
78
|
+
rails: list[str]
|
|
79
|
+
n_inferences: int
|
|
80
|
+
window_ms: float
|
|
81
|
+
sample_count: int
|
|
82
|
+
spread_mj: float | None = None # std/spread across M repeated windows;
|
|
83
|
+
# None for a single window
|
|
84
|
+
source: str = "measured"
|
|
85
|
+
scope: str = "carrier-rail-delta"
|
|
86
|
+
|
|
87
|
+
def __post_init__(self) -> None:
|
|
88
|
+
if self.source != "measured" or self.scope != "carrier-rail-delta":
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"EnergyMeasurement is board-level-delta only: source must be "
|
|
91
|
+
"'measured' and scope 'carrier-rail-delta' — never label this "
|
|
92
|
+
"NPU/U85/U55/M55 silicon energy.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class RunResult:
|
|
97
|
+
backend: str # "cpu-host" (reference) | on-device backend later
|
|
98
|
+
latency_ms: float # median wall-clock per inference
|
|
99
|
+
output_argmax: int | None
|
|
100
|
+
peak_sram_kib: float | None # None on host — on-device only
|
|
101
|
+
power_mj: float | None # None on host — on-board monitor read (HW-gated)
|
|
102
|
+
runs: int
|
|
103
|
+
energy: EnergyMeasurement | None = None # None until a real bench run populates it
|
|
104
|
+
# Set when the caller's sample was cast to the model input dtype (tan-cli#1497).
|
|
105
|
+
input_cast: str | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_ORT_DTYPES = {
|
|
109
|
+
"float": "float32", "float16": "float16", "double": "float64",
|
|
110
|
+
"int64": "int64", "int32": "int32", "int16": "int16", "int8": "int8",
|
|
111
|
+
"uint64": "uint64", "uint32": "uint32", "uint16": "uint16", "uint8": "uint8",
|
|
112
|
+
"bool": "bool",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _np_dtype(ort_type: str) -> str:
|
|
117
|
+
"""`tensor(int64)` -> `int64`; anything unrecognised (sequences, strings)
|
|
118
|
+
falls back to float32, the previous behaviour (tan-cli#1486)."""
|
|
119
|
+
inner = ort_type[ort_type.find("(") + 1:ort_type.rfind(")")] if "(" in ort_type else ort_type
|
|
120
|
+
return _ORT_DTYPES.get(inner, "float32")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _sample(shape: list, ort_type: str, seed: int) -> Any:
|
|
124
|
+
import numpy as np
|
|
125
|
+
dtype = _np_dtype(ort_type)
|
|
126
|
+
dims = [(1 if not isinstance(d, int) else d) for d in shape]
|
|
127
|
+
if np.dtype(dtype).kind == "f":
|
|
128
|
+
return np.random.default_rng(seed).standard_normal(dims).astype(dtype)
|
|
129
|
+
return np.zeros(dims, dtype=dtype) # token ids / pixels / flags: a valid, deterministic value
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def describe_input_cast(sample_dtype: Any, want: str) -> str | None:
|
|
133
|
+
"""A sentence when casting a `sample_dtype` sample to @want changes its
|
|
134
|
+
dtype (tan-cli#1497), naming the silent value loss when float becomes
|
|
135
|
+
integer/bool; None when no cast happens."""
|
|
136
|
+
import numpy as np
|
|
137
|
+
have = np.dtype(sample_dtype)
|
|
138
|
+
if have == np.dtype(want):
|
|
139
|
+
return None
|
|
140
|
+
msg = f"input sample dtype {have} was cast to the model input dtype {want}"
|
|
141
|
+
wd = np.dtype(want)
|
|
142
|
+
if wd.kind == "b":
|
|
143
|
+
msg += "; every nonzero value became True"
|
|
144
|
+
elif have.kind == "f" and wd.kind in "iu":
|
|
145
|
+
msg += "; fractional values were truncated toward zero"
|
|
146
|
+
elif not np.can_cast(have, wd, "safe"):
|
|
147
|
+
msg += "; precision or range may be lost"
|
|
148
|
+
return msg
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def default_input(onnx_path: Path, *, seed: int = 0) -> Any:
|
|
152
|
+
"""Deterministic random sample matching the model's first input shape.
|
|
153
|
+
Dynamic dims collapse to 1 (intent: a dynamic BATCH dim; a symbolic H/W
|
|
154
|
+
would also become 1). Wrapped in MeasureError so a bad model fails clean."""
|
|
155
|
+
import numpy as np
|
|
156
|
+
import onnxruntime as ort
|
|
157
|
+
try:
|
|
158
|
+
sess = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"])
|
|
159
|
+
first = sess.get_inputs()[0]
|
|
160
|
+
return _sample(first.shape, first.type, seed)
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
raise MeasureError(f"could not load model {Path(onnx_path).name}: {exc}") from exc
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run_host(onnx_path: Path, input_array: Any, *, runs: int = 20) -> RunResult:
|
|
166
|
+
import numpy as np
|
|
167
|
+
import onnxruntime as ort
|
|
168
|
+
try:
|
|
169
|
+
sess = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"])
|
|
170
|
+
inputs = sess.get_inputs()
|
|
171
|
+
# The sample feeds the FIRST input in that input's own dtype; any further
|
|
172
|
+
# inputs get a deterministic default sample (tan-cli#1486).
|
|
173
|
+
raw = np.asarray(input_array)
|
|
174
|
+
want = _np_dtype(inputs[0].type)
|
|
175
|
+
input_cast = describe_input_cast(raw.dtype, want)
|
|
176
|
+
feed = {inputs[0].name: raw.astype(want)}
|
|
177
|
+
for extra in inputs[1:]:
|
|
178
|
+
feed[extra.name] = _sample(extra.shape, extra.type, 0)
|
|
179
|
+
out = sess.run(None, feed)[0] # warm-up + a real output
|
|
180
|
+
times = []
|
|
181
|
+
for _ in range(max(1, runs)):
|
|
182
|
+
t0 = perf_counter()
|
|
183
|
+
sess.run(None, feed)
|
|
184
|
+
times.append((perf_counter() - t0) * 1000.0)
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
raise MeasureError(f"host run failed: {exc}") from exc
|
|
187
|
+
return RunResult(backend="cpu-host", latency_ms=round(median(times), 3),
|
|
188
|
+
output_argmax=int(np.asarray(out).ravel().argmax()),
|
|
189
|
+
peak_sram_kib=None, power_mj=None, runs=len(times),
|
|
190
|
+
input_cast=input_cast)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True)
|
|
194
|
+
class ABComparison:
|
|
195
|
+
faster: str # "a" | "b" | "tie"
|
|
196
|
+
latency_ratio: float | None # b.latency_ms / a.latency_ms; None if a.latency_ms == 0
|
|
197
|
+
# (avoids emitting non-finite JSON `Infinity`)
|
|
198
|
+
a_latency_ms: float
|
|
199
|
+
b_latency_ms: float
|
|
200
|
+
size_delta_bytes: int | None # size_b - size_a
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def compare(a: RunResult, b: RunResult, *, size_a: int | None = None,
|
|
204
|
+
size_b: int | None = None) -> ABComparison:
|
|
205
|
+
if a.latency_ms < b.latency_ms:
|
|
206
|
+
faster = "a"
|
|
207
|
+
elif b.latency_ms < a.latency_ms:
|
|
208
|
+
faster = "b"
|
|
209
|
+
else:
|
|
210
|
+
faster = "tie"
|
|
211
|
+
ratio = (b.latency_ms / a.latency_ms) if a.latency_ms else None
|
|
212
|
+
delta = (size_b - size_a) if (size_a is not None and size_b is not None) else None
|
|
213
|
+
return ABComparison(faster=faster,
|
|
214
|
+
latency_ratio=round(ratio, 4) if ratio is not None else None,
|
|
215
|
+
a_latency_ms=a.latency_ms, b_latency_ms=b.latency_ms,
|
|
216
|
+
size_delta_bytes=delta)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Schema validation for the per-NPU op-support tables `tan model check`
|
|
3
|
+
reads (tan-cli#1298).
|
|
4
|
+
|
|
5
|
+
`metadata/npu_ops/**` used to have no schema, so `analyze.py` carried only
|
|
6
|
+
`isinstance` guards. `metadata/schemas/npu-ops-v1.schema.json` now exists on
|
|
7
|
+
alp-sdk `dev` (alp-sdk#1801 closed, landed with alp-sdk#1470); a bound SDK may
|
|
8
|
+
or may not ship it. This module is the read-path half of tan-cli#964's rule
|
|
9
|
+
for that table family: validate on read, always report, and skip-but-disclose
|
|
10
|
+
when the schema cannot be used -- the policy lives here, the validator stays
|
|
11
|
+
the one in `tan.core.metadata_schema`.
|
|
12
|
+
|
|
13
|
+
Three outcomes for a table that is about to be used:
|
|
14
|
+
|
|
15
|
+
* schema usable, table valid -> used.
|
|
16
|
+
* schema usable, table violates it -> NOT used (skipped like a malformed
|
|
17
|
+
file, so the backend reads `undetermined` unless another table covers the
|
|
18
|
+
variant), with one note naming the file, JSON pointer and what was found.
|
|
19
|
+
* schema absent, unreadable, not valid JSON, or not a valid JSON Schema ->
|
|
20
|
+
the table IS used under `analyze.py`'s isinstance guards, with ONE note per
|
|
21
|
+
backend saying the tables were not validated. A broken schema is no
|
|
22
|
+
evidence against a table, so it is never blamed on one.
|
|
23
|
+
|
|
24
|
+
Only a table that matches the requested variant is validated; a table for
|
|
25
|
+
another variant is neither validated nor mentioned. Selection reads
|
|
26
|
+
`applies_to.variant` off a not-yet-validated document, which is safe because
|
|
27
|
+
`_table_variant` is isinstance-guarded to return `""` for any wrong shape --
|
|
28
|
+
and a table whose variant matches is then validated before it is accepted, so
|
|
29
|
+
an unvalidated document can only ever fail to match, never be scored.
|
|
30
|
+
|
|
31
|
+
The notes ride the static-screen report only. The `--exact` compiled report
|
|
32
|
+
and the bench-point report build their own `notes` and do not carry them.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from tan.core.metadata_schema import (
|
|
40
|
+
missing_schema_note,
|
|
41
|
+
npu_ops_schema_path,
|
|
42
|
+
schema_unusable_reason,
|
|
43
|
+
validate_document,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# A badly broken table can violate the schema dozens of times; the first few
|
|
47
|
+
# name the problem, the rest just bloat the envelope.
|
|
48
|
+
_MAX_VIOLATIONS_SHOWN = 5
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _posix(value: object) -> str:
|
|
52
|
+
return str(value).replace("\\", "/")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TableValidator:
|
|
56
|
+
"""Validates the tables of ONE backend directory against the npu-ops
|
|
57
|
+
schema, working out whether the schema is usable lazily -- on the first
|
|
58
|
+
table that is actually about to be used -- and disclosing "not validated"
|
|
59
|
+
at most once.
|
|
60
|
+
|
|
61
|
+
Every path in a note is written relative to the metadata root, with
|
|
62
|
+
forward slashes (`npu_ops/ethos_u/u85@vela-5.2.0.json`,
|
|
63
|
+
`schemas/npu-ops-v1.schema.json`): the note rides the envelope, and an
|
|
64
|
+
absolute host path in it is both noise and a leak. (It also made a test
|
|
65
|
+
whose tmp dir name contained a word the note-scanning tests forbid fail
|
|
66
|
+
on CI, tan-cli#1298.)"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, metadata_root: Path | str, table_dir: Path) -> None:
|
|
69
|
+
self._root = Path(metadata_root)
|
|
70
|
+
self._schema_path = npu_ops_schema_path(metadata_root)
|
|
71
|
+
self._table_dir = table_dir
|
|
72
|
+
self._probed = False
|
|
73
|
+
self._usable = False
|
|
74
|
+
|
|
75
|
+
def _rel(self, path: Path | str) -> str:
|
|
76
|
+
try:
|
|
77
|
+
return Path(path).relative_to(self._root).as_posix()
|
|
78
|
+
except ValueError:
|
|
79
|
+
return Path(path).name
|
|
80
|
+
|
|
81
|
+
def _scrub(self, text: str) -> str:
|
|
82
|
+
"""@text with the metadata root's absolute spellings replaced -- a
|
|
83
|
+
defence for a reason string that carries a path despite
|
|
84
|
+
`schema_unusable_reason` already dropping the OS filename."""
|
|
85
|
+
for spelling in {str(self._root), self._root.as_posix(), _posix(self._root)}:
|
|
86
|
+
text = text.replace(spelling, "<metadata>")
|
|
87
|
+
return text
|
|
88
|
+
|
|
89
|
+
def _probe(self, notes: list[str]) -> None:
|
|
90
|
+
self._probed = True
|
|
91
|
+
where = f"{self._rel(self._table_dir)}: not validated -- "
|
|
92
|
+
schema = self._rel(self._schema_path)
|
|
93
|
+
reason = schema_unusable_reason(self._schema_path)
|
|
94
|
+
if reason is not None:
|
|
95
|
+
notes.append(f"{where}the schema at {schema} could not be loaded "
|
|
96
|
+
f"({self._scrub(reason)}); the tables are used under "
|
|
97
|
+
"the built-in shape checks only")
|
|
98
|
+
return
|
|
99
|
+
try:
|
|
100
|
+
absent = missing_schema_note(self._schema_path, source=self._table_dir) is not None
|
|
101
|
+
except OSError as exc:
|
|
102
|
+
# `Path.is_file()` raises on a permission-denied ancestor on
|
|
103
|
+
# Python 3.12/3.13 (see the comment above `_is_table_file` in
|
|
104
|
+
# analyze.py): "cannot tell" is not "absent".
|
|
105
|
+
notes.append(f"{where}the schema at {schema} could not be inspected "
|
|
106
|
+
f"({exc.strerror or 'OS error'}); the tables are used "
|
|
107
|
+
"under the built-in shape checks only")
|
|
108
|
+
return
|
|
109
|
+
if absent:
|
|
110
|
+
notes.append(f"{where}no schema at {schema} in this checkout")
|
|
111
|
+
return
|
|
112
|
+
self._usable = True
|
|
113
|
+
|
|
114
|
+
def accepts(self, doc: object, table_path: Path, notes: list[str]) -> bool:
|
|
115
|
+
"""True when *table_path* (already parsed to *doc*) may be used.
|
|
116
|
+
Appends to *notes* the not-validated disclosure (once) or this
|
|
117
|
+
table's violation note."""
|
|
118
|
+
if not self._probed:
|
|
119
|
+
self._probe(notes)
|
|
120
|
+
if not self._usable:
|
|
121
|
+
return True
|
|
122
|
+
errors = validate_document(doc, self._schema_path, self._rel(table_path))
|
|
123
|
+
if not errors:
|
|
124
|
+
return True
|
|
125
|
+
shown = "; ".join(errors[:_MAX_VIOLATIONS_SHOWN])
|
|
126
|
+
extra = len(errors) - _MAX_VIOLATIONS_SHOWN
|
|
127
|
+
more = f" (+{extra} more)" if extra > 0 else ""
|
|
128
|
+
notes.append(f"npu-ops table skipped, it fails npu-ops-v1.schema.json: "
|
|
129
|
+
f"{shown}{more}. This table is not used.")
|
|
130
|
+
return False
|
tan/model/package.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Assemble/parse the .alpmodel binary container (header + CBOR manifest + blobs).
|
|
3
|
+
|
|
4
|
+
`CONTAINER_VERSION` describes the BINARY FRAME below -- the 24-byte header's
|
|
5
|
+
field layout and the 8-byte blob-table entries -- and nothing about which keys
|
|
6
|
+
the CBOR manifest carries. Adding a manifest key is therefore NOT a container
|
|
7
|
+
change: alp-sdk's on-device reader skips keys it does not know
|
|
8
|
+
(`src/common/alp_model.c`, see `tan.model.manifest`'s module docstring for the
|
|
9
|
+
measured evidence), while a CONTAINER_VERSION it does not know is a hard
|
|
10
|
+
ALP_ERR_VERSION refusal for every fielded device. Do not move this constant to
|
|
11
|
+
announce a manifest field.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
import os
|
|
15
|
+
import struct
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from .manifest import Manifest
|
|
18
|
+
|
|
19
|
+
MAGIC = b"ALPM"
|
|
20
|
+
CONTAINER_VERSION = 1
|
|
21
|
+
_HEADER = struct.Struct("<4sHHIIII") # magic, ver, flags, mft_off, mft_len, tbl_off, blob_count
|
|
22
|
+
_HEADER_SIZE = _HEADER.size # 24
|
|
23
|
+
_TBL_ENTRY = struct.Struct("<II") # blob off, blob len
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def write_package(mft: Manifest, blobs: list[bytes]) -> bytes:
|
|
27
|
+
mft_bytes = mft.to_cbor()
|
|
28
|
+
mft_off = _HEADER_SIZE
|
|
29
|
+
tbl_off = mft_off + len(mft_bytes)
|
|
30
|
+
blobs_off = tbl_off + _TBL_ENTRY.size * len(blobs)
|
|
31
|
+
|
|
32
|
+
table, blob_region, cursor = b"", b"", blobs_off
|
|
33
|
+
for b in blobs:
|
|
34
|
+
table += _TBL_ENTRY.pack(cursor, len(b))
|
|
35
|
+
blob_region += b
|
|
36
|
+
cursor += len(b)
|
|
37
|
+
|
|
38
|
+
header = _HEADER.pack(MAGIC, CONTAINER_VERSION, 0,
|
|
39
|
+
mft_off, len(mft_bytes), tbl_off, len(blobs))
|
|
40
|
+
return header + mft_bytes + table + blob_region
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _unpack_header(head: bytes) -> tuple[int, int, int, int]:
|
|
44
|
+
"""(mft_off, mft_len, tbl_off, blob_count) from a validated 24-byte header."""
|
|
45
|
+
magic, ver, _flags, mft_off, mft_len, tbl_off, n = _HEADER.unpack_from(head, 0)
|
|
46
|
+
if magic != MAGIC:
|
|
47
|
+
raise ValueError(f"bad magic {magic!r}")
|
|
48
|
+
if ver != CONTAINER_VERSION:
|
|
49
|
+
raise ValueError(f"unsupported container version {ver}")
|
|
50
|
+
return mft_off, mft_len, tbl_off, n
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def read_manifest_file(path: Path) -> Manifest:
|
|
54
|
+
"""The manifest of an on-disk package, WITHOUT reading its blobs.
|
|
55
|
+
|
|
56
|
+
`read_package` needs the whole container in memory; a caller that only
|
|
57
|
+
wants to read back what a package says about itself (`tan model build`
|
|
58
|
+
reporting a shipped target's caveats) would then pay a full copy of every
|
|
59
|
+
compiled blob -- megabytes for a real vision model -- to read a few
|
|
60
|
+
hundred bytes of CBOR. This seeks to the manifest region instead, so the
|
|
61
|
+
cost is O(manifest), not O(package).
|
|
62
|
+
|
|
63
|
+
Reading it back off disk, rather than reporting the in-memory object that
|
|
64
|
+
was just written, is deliberate: it reports what the ARTIFACT says. A
|
|
65
|
+
caveat lost by the encoder is then visibly missing from the build output
|
|
66
|
+
instead of being narrated from a manifest that never reached the file."""
|
|
67
|
+
with open(path, "rb") as f:
|
|
68
|
+
head = f.read(_HEADER_SIZE)
|
|
69
|
+
if len(head) < _HEADER_SIZE:
|
|
70
|
+
raise ValueError(f"truncated container: {len(head)} bytes, "
|
|
71
|
+
f"header is {_HEADER_SIZE}")
|
|
72
|
+
mft_off, mft_len, _tbl_off, _n = _unpack_header(head)
|
|
73
|
+
# Bound the read against the real file size BEFORE issuing it.
|
|
74
|
+
# `mft_off`/`mft_len` are untrusted u32s off the wire, and unlike
|
|
75
|
+
# `read_package`'s slice of an already-loaded buffer, `f.read(n)`
|
|
76
|
+
# allocates n bytes -- an `mft_len` of 0xFFFFFFFF would ask for 4 GiB
|
|
77
|
+
# from a 5 KiB file. Compared subtraction-style, the same shape
|
|
78
|
+
# alp-sdk's reader uses for the same fields (`src/common/alp_model.c`:
|
|
79
|
+
# "adding the untrusted u32 offset and length first can wrap").
|
|
80
|
+
size = os.fstat(f.fileno()).st_size
|
|
81
|
+
if mft_off > size or mft_len > size - mft_off:
|
|
82
|
+
raise ValueError(f"manifest region {mft_off}+{mft_len} runs past "
|
|
83
|
+
f"the end of a {size}-byte container")
|
|
84
|
+
f.seek(mft_off)
|
|
85
|
+
blob = f.read(mft_len)
|
|
86
|
+
if len(blob) != mft_len:
|
|
87
|
+
raise ValueError(f"truncated manifest: {len(blob)} of {mft_len} bytes")
|
|
88
|
+
return Manifest.from_cbor(blob)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def read_package(raw: bytes) -> tuple[Manifest, list[bytes]]:
|
|
92
|
+
"""The manifest AND blobs of an in-memory `.alpmodel` buffer.
|
|
93
|
+
|
|
94
|
+
Every region this reads (`mft_off`/`mft_len`, the blob table itself, and
|
|
95
|
+
each table entry's `off`/`length`) is bounds-checked against `len(raw)`
|
|
96
|
+
before the matching slice, using the same subtraction-style compare
|
|
97
|
+
`read_manifest_file` uses (`off > size or length > size - off`, chosen
|
|
98
|
+
there because adding the untrusted u32 offset and length first can wrap).
|
|
99
|
+
The motivation for checking at all differs, though: `read_manifest_file`
|
|
100
|
+
checks to avoid an over-allocation risk (`f.read(n)` allocates n bytes up
|
|
101
|
+
front), while slicing `bytes` cannot over-allocate but also does not
|
|
102
|
+
raise -- `b"AB"[0:999999]` is `b"AB"`, silently truncated, not a crash
|
|
103
|
+
(tan-cli#1045). A corrupted or adversarial header/table entry must be
|
|
104
|
+
refused here, the same way `read_manifest_file`'s file-based sibling
|
|
105
|
+
already refuses one."""
|
|
106
|
+
size = len(raw)
|
|
107
|
+
if size < _HEADER_SIZE:
|
|
108
|
+
raise ValueError(f"truncated container: {size} bytes, header is {_HEADER_SIZE}")
|
|
109
|
+
mft_off, mft_len, tbl_off, n = _unpack_header(raw)
|
|
110
|
+
if mft_off > size or mft_len > size - mft_off:
|
|
111
|
+
raise ValueError(f"manifest region {mft_off}+{mft_len} runs past "
|
|
112
|
+
f"the end of a {size}-byte container")
|
|
113
|
+
mft = Manifest.from_cbor(raw[mft_off:mft_off + mft_len])
|
|
114
|
+
tbl_len = n * _TBL_ENTRY.size
|
|
115
|
+
if tbl_off > size or tbl_len > size - tbl_off:
|
|
116
|
+
raise ValueError(f"blob table region {tbl_off}+{tbl_len} (for {n} "
|
|
117
|
+
f"entries) runs past the end of a {size}-byte container")
|
|
118
|
+
blobs = []
|
|
119
|
+
for i in range(n):
|
|
120
|
+
off, length = _TBL_ENTRY.unpack_from(raw, tbl_off + i * _TBL_ENTRY.size)
|
|
121
|
+
if off > size or length > size - off:
|
|
122
|
+
raise ValueError(f"blob {i} region {off}+{length} runs past "
|
|
123
|
+
f"the end of a {size}-byte container")
|
|
124
|
+
blobs.append(raw[off:off + length])
|
|
125
|
+
return mft, blobs
|