mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 23:06:18 +00:00
ESP32-family app and bootloader images (magic 0xE9) had no reader, so open_binary refused OTA downloads, app partitions and bootloaders. Add the esp-image container format behind inspect_artifact and extract_artifact. A pure decoder reads each byte once and keeps only fixed-size fields. Derived facts cover the chip, from a table cited to ESP-IDF v6.1; segment classification as ESP-IDF's should_map and should_load; checksum and appended SHA-256 results; app and bootloader descriptors; Secure Boot v2 signature blocks, observed and unverified; and undescribed trailing byte ranges. Truncation and undescribed bytes make coverage unknown. ESP8266 images are refused with a specific reason, and ciphertext stays unrecognized. Add the format to every target, Evidence, snapshot, live-summary and artifact vocabulary, document it, and add an optional verify:esp-images lane that compares esptool-built images with esptool image-info and espsecure signature-info-v2. Refs #3615
285 lines
9.6 KiB
TypeScript
285 lines
9.6 KiB
TypeScript
import { z } from "zod";
|
|
|
|
const offset = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
const uint32 = z.number().int().min(0).max(0xffff_ffff);
|
|
|
|
/** Parsed source fields owned by the PCK adapter, independent of content hashes. */
|
|
export const godotPckMetadataSchema = z.strictObject({
|
|
kind: z.literal("godot-pck"),
|
|
format_version: uint32,
|
|
engine_major: uint32,
|
|
engine_minor: uint32,
|
|
engine_patch: uint32,
|
|
pack_start: offset,
|
|
pack_end: offset,
|
|
embedded: z.boolean(),
|
|
pack_flags: uint32,
|
|
file_base: offset,
|
|
directory_start: offset,
|
|
declared_file_count: uint32,
|
|
directory_status: z.enum(["available", "encrypted", "sparse-bundle"]),
|
|
});
|
|
export type GodotPckMetadata = z.infer<typeof godotPckMetadataSchema>;
|
|
|
|
export const godotPckEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("godot-pck-entry"),
|
|
index: uint32,
|
|
source_path: z.string(),
|
|
declared_offset: offset,
|
|
flags: uint32,
|
|
md5_status: z.enum(["declared", "all-zero"]),
|
|
});
|
|
|
|
/** Source cookie fields and derived directory counts without Python execution. */
|
|
export const pyInstallerMetadataSchema = z.strictObject({
|
|
kind: z.literal("pyinstaller"),
|
|
layout_version: z.enum(["2.0", "2.1"]),
|
|
archive_start: offset,
|
|
archive_size: offset,
|
|
cookie_offset: offset,
|
|
toc_offset: offset,
|
|
toc_size: offset,
|
|
entry_count: offset,
|
|
runtime_option_count: offset,
|
|
external_dependency_count: offset,
|
|
python_version_raw: uint32,
|
|
python_major: uint32.nullable(),
|
|
python_minor: uint32.nullable(),
|
|
python_library: z.string().nullable(),
|
|
encryption_marker_present: z.boolean(),
|
|
});
|
|
export type PyInstallerMetadata = z.infer<typeof pyInstallerMetadataSchema>;
|
|
|
|
/** Primitive facts preserve source spelling and keep metadata copies independent. */
|
|
export const pyInstallerEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("pyinstaller-entry"),
|
|
container: z.enum(["carchive", "pyz"]),
|
|
container_name: z.string().nullable(),
|
|
index: offset,
|
|
source_name: z.string(),
|
|
type_code: z.union([z.string().length(1), uint32]),
|
|
relative_offset: offset,
|
|
directory_record_offset: offset.nullable(),
|
|
directory_record_size: offset.nullable(),
|
|
compression_flag: z.union([z.literal(0), z.literal(1)]),
|
|
bytecode_header: z.enum(["not-bytecode", "unknown", "derived"]),
|
|
bytecode_magic: z
|
|
.string()
|
|
.regex(/^[0-9a-f]{8}$/u)
|
|
.nullable(),
|
|
pyz_status: z.enum(["not-pyz", "decoded", "opaque"]),
|
|
pyz_sha256: z
|
|
.string()
|
|
.regex(/^[0-9a-f]{64}$/u)
|
|
.nullable(),
|
|
pyz_python_magic: z
|
|
.string()
|
|
.regex(/^[0-9a-f]{8}$/u)
|
|
.nullable(),
|
|
pyz_python_version: z.string().nullable(),
|
|
pyz_entry_count: offset.nullable(),
|
|
encryption_source: z.literal("carchive-marker").nullable(),
|
|
dependency_status: z
|
|
.enum(["unresolved", "invalid-reference"])
|
|
.exactOptional(),
|
|
dependency_archive: z.string().nullable().exactOptional(),
|
|
dependency_member: z.string().nullable().exactOptional(),
|
|
});
|
|
export type PyInstallerEntryMetadata = z.infer<
|
|
typeof pyInstallerEntryMetadataSchema
|
|
>;
|
|
|
|
/**
|
|
* Node.js single-executable blob facts. Lengths follow the Node 24 64-bit
|
|
* little-endian `size_t` reader. The fuse sentinel is a separate carrier scan.
|
|
*/
|
|
export const nodeSeaMetadataSchema = z.strictObject({
|
|
kind: z.literal("node-sea"),
|
|
magic: z.literal(0x143da20),
|
|
flags: uint32,
|
|
disable_experimental_sea_warning: z.boolean(),
|
|
use_snapshot: z.boolean(),
|
|
use_code_cache: z.boolean(),
|
|
include_assets: z.boolean(),
|
|
include_exec_argv: z.boolean(),
|
|
exec_argv_extension: z.enum(["none", "env", "cli"]),
|
|
exec_argv: z.array(z.string()),
|
|
code_path: z.string(),
|
|
carrier: z.enum(["mach-o", "elf", "pe"]),
|
|
blob_offset: offset,
|
|
blob_length: offset,
|
|
fuse_state: z.enum(["set", "unset", "absent", "conflicting"]),
|
|
fuse_sentinel: z.string().nullable(),
|
|
fuse_contradiction: z.string().nullable(),
|
|
});
|
|
export type NodeSeaMetadata = z.infer<typeof nodeSeaMetadataSchema>;
|
|
|
|
/** One injected script, snapshot, code cache, or asset. Bytes stay opaque. */
|
|
export const nodeSeaEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("node-sea-entry"),
|
|
role: z.enum(["main-script", "snapshot", "code-cache", "asset"]),
|
|
source_path: z.string(),
|
|
index: uint32,
|
|
});
|
|
|
|
/** Runtime member identity, independent of blob layout and directory order. */
|
|
export const nodeSeaEntryDeclaration = (
|
|
metadata: z.infer<typeof nodeSeaEntryMetadataSchema>,
|
|
) => ({ role: metadata.role, source_path: metadata.source_path });
|
|
|
|
const uint8 = z.number().int().min(0).max(0xff);
|
|
const uint16 = z.number().int().min(0).max(0xffff);
|
|
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/u);
|
|
const byteRange = z.strictObject({ offset, length: offset });
|
|
|
|
/** `esp_app_desc_t` fields, as ESP-IDF names them; strings stop at NUL. */
|
|
const espAppDescriptorSchema = z.strictObject({
|
|
kind: z.literal("app"),
|
|
offset,
|
|
secure_version: uint32,
|
|
version: z.string().nullable(),
|
|
project_name: z.string().nullable(),
|
|
time: z.string().nullable(),
|
|
date: z.string().nullable(),
|
|
idf_ver: z.string().nullable(),
|
|
app_elf_sha256: sha256Hex,
|
|
min_efuse_blk_rev_full: uint16,
|
|
max_efuse_blk_rev_full: uint16,
|
|
/** Stored as log2; 0 in images built before ESP-IDF v5.4. */
|
|
mmu_page_size: uint8,
|
|
});
|
|
|
|
/** `esp_bootloader_desc_t` fields, as ESP-IDF names them. */
|
|
const espBootloaderDescriptorSchema = z.strictObject({
|
|
kind: z.literal("bootloader"),
|
|
offset,
|
|
secure_version: uint8,
|
|
version: uint32,
|
|
idf_ver: z.string().nullable(),
|
|
date_time: z.string().nullable(),
|
|
});
|
|
|
|
/**
|
|
* ESP32-family image facts. `header` and the descriptor fields are observed;
|
|
* the descriptor kind, `chip`, the integrity statuses and the trailing-byte
|
|
* ranges are derived.
|
|
*/
|
|
export const espImageMetadataSchema = z.strictObject({
|
|
kind: z.literal("esp-image"),
|
|
/** `esp_image_header_t` fields, as ESP-IDF names them. */
|
|
header: z.strictObject({
|
|
segment_count: uint8,
|
|
spi_mode: uint8,
|
|
spi_speed: uint8,
|
|
spi_size: uint8,
|
|
entry_addr: uint32,
|
|
wp_pin: uint8,
|
|
spi_pin_drv: z.string().regex(/^[0-9a-f]{6}$/u),
|
|
chip_id: uint16,
|
|
min_chip_rev: uint8,
|
|
min_chip_rev_full: uint16,
|
|
max_chip_rev_full: uint16,
|
|
reserved: z.string().regex(/^[0-9a-f]{8}$/u),
|
|
hash_appended: z.boolean(),
|
|
}),
|
|
/** Derived from `chip_id` through the cited table; null for unknown IDs. */
|
|
chip: z
|
|
.strictObject({
|
|
target: z.string(),
|
|
isa: z.enum(["xtensa", "riscv"]),
|
|
source: z.string(),
|
|
})
|
|
.nullable(),
|
|
checksum: z.strictObject({
|
|
offset: offset.nullable(),
|
|
stored: uint8.nullable(),
|
|
computed: uint8.nullable(),
|
|
status: z.enum(["valid", "invalid", "unavailable"]),
|
|
}),
|
|
/** The SHA-256 the header's `hash_appended` adds after the checksum. */
|
|
appended_sha256: z.strictObject({
|
|
offset: offset.nullable(),
|
|
stored: sha256Hex.nullable(),
|
|
computed: sha256Hex.nullable(),
|
|
status: z.enum(["valid", "invalid", "unavailable", "not-appended"]),
|
|
}),
|
|
/** End of the checksum block and any digest; null when truncated before it. */
|
|
image_end: offset.nullable(),
|
|
signature_sector_offset: offset.nullable(),
|
|
/** Bytes after the image that no padding, signature or erased flash describes. */
|
|
undescribed_trailing: z.array(byteRange),
|
|
/** Its kind classifies the image as an app or a bootloader. */
|
|
descriptor: z
|
|
.discriminatedUnion("kind", [
|
|
espAppDescriptorSchema,
|
|
espBootloaderDescriptorSchema,
|
|
])
|
|
.nullable(),
|
|
});
|
|
export type EspImageMetadata = z.infer<typeof espImageMetadataSchema>;
|
|
|
|
/** One header-declared segment; `data_len` is declared, not available. */
|
|
export const espImageSegmentMetadataSchema = z.strictObject({
|
|
kind: z.literal("esp-image-segment"),
|
|
index: uint8,
|
|
load_addr: uint32,
|
|
data_len: uint32,
|
|
header_offset: offset,
|
|
/** ESP-IDF's cold-boot `should_map` and `should_load`; derived. */
|
|
classification: z.enum(["mapped", "loaded", "not-loaded", "unknown"]),
|
|
});
|
|
export type EspImageSegmentMetadata = z.infer<
|
|
typeof espImageSegmentMetadataSchema
|
|
>;
|
|
|
|
/** One Secure Boot v2 block with its 0xE7 magic and a matching CRC32. */
|
|
export const espImageSignatureBlockMetadataSchema = z.strictObject({
|
|
kind: z.literal("esp-image-signature-block"),
|
|
/** Slot in the signature sector, 0 to 2. */
|
|
index: uint8,
|
|
version: uint8,
|
|
scheme: z.enum(["rsa", "ecdsa"]).nullable(),
|
|
/** Third block byte for ECDSA: 0 selects SHA-256, 1 SHA-384. */
|
|
sha_version: uint8.nullable(),
|
|
curve_id: uint8.nullable(),
|
|
digest_algorithm: z.enum(["sha256", "sha384"]).nullable(),
|
|
image_digest: z
|
|
.string()
|
|
.regex(/^(?:[0-9a-f]{64}|[0-9a-f]{96})$/u)
|
|
.nullable(),
|
|
/** Whether `image_digest` equals the digest of the bytes before the sector. */
|
|
image_digest_matches: z.boolean().nullable(),
|
|
/** SHA-256 of the block's public key, the value eFuse key digests hold. */
|
|
public_key_sha256: sha256Hex.nullable(),
|
|
/** REA checks no key, signature or eFuse. */
|
|
signature_status: z.literal("unverified"),
|
|
});
|
|
export type EspImageSignatureBlockMetadata = z.infer<
|
|
typeof espImageSignatureBlockMetadataSchema
|
|
>;
|
|
|
|
/** Declaration identity that is stable across archive layout changes. */
|
|
export const pyInstallerEntryDeclaration = (
|
|
metadata: PyInstallerEntryMetadata,
|
|
) => ({
|
|
container: metadata.container,
|
|
container_name: metadata.container_name,
|
|
source_name: metadata.source_name,
|
|
type_code: metadata.type_code,
|
|
});
|
|
|
|
export const artifactFormatMetadataSchema = z.discriminatedUnion("kind", [
|
|
godotPckMetadataSchema,
|
|
godotPckEntryMetadataSchema,
|
|
pyInstallerMetadataSchema,
|
|
pyInstallerEntryMetadataSchema,
|
|
nodeSeaMetadataSchema,
|
|
nodeSeaEntryMetadataSchema,
|
|
espImageMetadataSchema,
|
|
espImageSegmentMetadataSchema,
|
|
espImageSignatureBlockMetadataSchema,
|
|
]);
|
|
export type ArtifactFormatMetadata = z.infer<
|
|
typeof artifactFormatMetadataSchema
|
|
>;
|