mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
1198 lines
40 KiB
TypeScript
1198 lines
40 KiB
TypeScript
import {
|
|
ntfsFileReferenceSchema,
|
|
ntfsFileNameSchema,
|
|
ntfsAttributeSchema,
|
|
ntfsAttributeIssueSchema,
|
|
ntfsAttributeListResolutionSchema,
|
|
} from "./windows/ntfsMftAttributes.js";
|
|
import { z } from "zod";
|
|
import { MFT_INLINE_HEADER_LIMIT } from "./windows/ntfsMft.js";
|
|
|
|
import { jsonRecordSchema } from "./jsonRecord.js";
|
|
import { jsonValueSchema } from "./jsonValue.js";
|
|
|
|
const offset = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
export 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"]),
|
|
});
|
|
|
|
/** Parsed source fields owned by the UnityFS adapter, independent of member bytes. */
|
|
export const unityFsMetadataSchema = z.strictObject({
|
|
kind: z.literal("unityfs"),
|
|
signature: z.string(),
|
|
format_version: uint32,
|
|
player_version: z.string(),
|
|
engine_version: z.string(),
|
|
declared_size: offset,
|
|
flags: uint32,
|
|
blocks_info_compressed_size: uint32,
|
|
blocks_info_uncompressed_size: uint32,
|
|
blocks_info_at_end: z.boolean(),
|
|
data_start: offset.nullable(),
|
|
block_count: uint32.nullable(),
|
|
/** The 16-byte producer hash of the uncompressed block stream, as declared. */
|
|
data_hash: z.string().nullable(),
|
|
directory_status: z.enum([
|
|
"available",
|
|
"encrypted",
|
|
"unsupported-compression",
|
|
"unsupported-layout",
|
|
]),
|
|
});
|
|
export type UnityFsMetadata = z.infer<typeof unityFsMetadataSchema>;
|
|
|
|
export const unityFsEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("unityfs-entry"),
|
|
index: uint32,
|
|
source_path: z.string(),
|
|
declared_offset: offset,
|
|
flags: uint32,
|
|
});
|
|
|
|
/** Parsed source fields owned by the wxapkg adapter, independent of member bytes. */
|
|
export const wxapkgMetadataSchema = z.strictObject({
|
|
kind: z.literal("wxapkg"),
|
|
info: uint32.nullable(),
|
|
index_length: uint32.nullable(),
|
|
data_length: uint32.nullable(),
|
|
file_count: uint32.nullable(),
|
|
directory_status: z.enum(["available", "encrypted"]),
|
|
});
|
|
export type WxapkgMetadata = z.infer<typeof wxapkgMetadataSchema>;
|
|
|
|
export const wxapkgEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("wxapkg-entry"),
|
|
index: uint32,
|
|
source_path: z.string(),
|
|
declared_offset: offset,
|
|
});
|
|
|
|
/** 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
|
|
>;
|
|
|
|
/** Ordinary .NET bundle manifest facts, not proof of managed assembly validity. */
|
|
export const dotnetBundleEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("dotnet-bundle-entry"),
|
|
major_version: uint32,
|
|
minor_version: uint32,
|
|
file_type: z.number().int().min(0).max(255),
|
|
});
|
|
|
|
/**
|
|
* 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,
|
|
});
|
|
|
|
/**
|
|
* UF2 facts derived from every 512-byte block. Regions hold only valid
|
|
* main-flash payloads; block numbering is grouped by familyID and block count.
|
|
*/
|
|
export const uf2MetadataSchema = z.strictObject({
|
|
kind: z.literal("uf2"),
|
|
block_count: offset,
|
|
trailing_bytes: offset,
|
|
invalid_block_count: offset,
|
|
flash_block_count: offset,
|
|
not_main_flash_block_count: offset,
|
|
file_container_block_count: offset,
|
|
md5_block_count: offset,
|
|
extension_tag_block_count: offset,
|
|
family_count: offset,
|
|
/** The registry revision REA's region `family_name` values come from. */
|
|
family_name_registry: z.string(),
|
|
numbering_group_count: offset,
|
|
missing_block_count: offset,
|
|
ambiguous_numbering_group_count: offset,
|
|
repeated_block_count: offset,
|
|
out_of_order_block_count: offset,
|
|
block_number_out_of_range_count: offset,
|
|
region_count: offset,
|
|
data_bytes: offset,
|
|
duplicate_bytes: offset,
|
|
});
|
|
export type Uf2Metadata = z.infer<typeof uf2MetadataSchema>;
|
|
|
|
/** One family's address-contiguous region; file blocks are zero-based positions. */
|
|
export const uf2RegionMetadataSchema = z.strictObject({
|
|
kind: z.literal("uf2-region"),
|
|
family_id: uint32.nullable(),
|
|
/** REA's label for `family_id`, from `family_name_registry`; not file content. */
|
|
family_name: z.string().nullable(),
|
|
start_address: uint32,
|
|
end_address: uint32,
|
|
length: offset,
|
|
block_count: offset,
|
|
first_file_block: offset,
|
|
last_file_block: offset,
|
|
});
|
|
|
|
/** DfuSe prefix and DFU suffix facts; both CRCs use the suffix's stored form. */
|
|
export const dfuSeMetadataSchema = z.strictObject({
|
|
kind: z.literal("dfuse"),
|
|
image_size: uint32,
|
|
target_count: uint32,
|
|
element_count: offset,
|
|
empty_element_count: offset,
|
|
overlapping_element_count: offset,
|
|
data_bytes: offset,
|
|
device_version: uint32,
|
|
product_id: uint32,
|
|
vendor_id: uint32,
|
|
suffix_crc: uint32,
|
|
computed_crc: uint32,
|
|
});
|
|
export type DfuSeMetadata = z.infer<typeof dfuSeMetadataSchema>;
|
|
|
|
/** One DfuSe element with its target's alternate setting and name. */
|
|
export const dfuSeElementMetadataSchema = z.strictObject({
|
|
kind: z.literal("dfuse-element"),
|
|
target_index: uint32,
|
|
alternate_setting: uint32,
|
|
target_name: z.string().nullable(),
|
|
element_index: uint32,
|
|
start_address: uint32,
|
|
end_address: uint32,
|
|
length: offset,
|
|
});
|
|
|
|
/**
|
|
* Intel HEX or Motorola S-record facts derived from every record. Start
|
|
* addresses are declared values, not proven entry points.
|
|
*/
|
|
export const hexRecordsMetadataSchema = z.strictObject({
|
|
kind: z.literal("hex-records"),
|
|
encoding: z.enum(["intel-hex", "srec"]),
|
|
variant: z
|
|
.enum(["I8HEX", "I16HEX", "I32HEX", "S19", "S28", "S37", "mixed"])
|
|
.nullable(),
|
|
line_count: offset,
|
|
record_count: offset,
|
|
data_record_count: offset,
|
|
data_bytes: offset,
|
|
duplicate_bytes: offset,
|
|
region_count: offset,
|
|
lowest_address: uint32.nullable(),
|
|
highest_address: uint32.nullable(),
|
|
termination: z.enum(["present", "absent"]),
|
|
start_address: uint32.nullable(),
|
|
start_address_record: z.enum(["05", "S7", "S8", "S9"]).nullable(),
|
|
start_segment_cs: uint32.nullable(),
|
|
start_segment_ip: uint32.nullable(),
|
|
header_hex: z
|
|
.string()
|
|
.regex(/^(?:[0-9a-f]{2}){0,252}$/u)
|
|
.nullable(),
|
|
header_text: z.string().nullable(),
|
|
declared_data_record_count: uint32.nullable(),
|
|
});
|
|
export type HexRecordsMetadata = z.infer<typeof hexRecordsMetadataSchema>;
|
|
|
|
/** One address-contiguous region; lines span its contributing data records. */
|
|
export const hexRecordRegionMetadataSchema = z.strictObject({
|
|
kind: z.literal("hex-record-region"),
|
|
start_address: uint32,
|
|
end_address: uint32,
|
|
length: offset,
|
|
record_count: offset,
|
|
first_line: offset,
|
|
last_line: offset,
|
|
});
|
|
|
|
const ntfsRecordHeaderSchema = z.strictObject({
|
|
index: offset,
|
|
fileOffset: offset,
|
|
sequenceNumber: offset,
|
|
hardLinkCount: offset,
|
|
flags: z.strictObject({
|
|
raw: offset,
|
|
names: z.array(z.string()),
|
|
unknownMask: offset,
|
|
}),
|
|
inUse: z.boolean(),
|
|
directory: z.boolean(),
|
|
usedBytes: offset,
|
|
totalBytes: offset,
|
|
extentValid: z.boolean(),
|
|
baseRecordReference: ntfsFileReferenceSchema.nullable(),
|
|
declaredRecordNumber: offset.nullable(),
|
|
recordNumberMatchesSlot: z.boolean().nullable(),
|
|
logFileSequenceNumber: z.string().regex(/^(?:0|[1-9][0-9]*)$/u),
|
|
firstAttributeOffset: offset,
|
|
nextAttributeId: offset,
|
|
fileNames: z.array(ntfsFileNameSchema).exactOptional(),
|
|
attributes: z.array(ntfsAttributeSchema).exactOptional(),
|
|
attributeCoverage: z.enum(["complete", "incomplete"]).exactOptional(),
|
|
attributeIssue: ntfsAttributeIssueSchema.nullable().exactOptional(),
|
|
fileNameCoverage: z.enum(["complete", "incomplete"]).exactOptional(),
|
|
attributeListResolution: ntfsAttributeListResolutionSchema.exactOptional(),
|
|
fixups: z.strictObject({
|
|
sequenceNumber: offset,
|
|
entries: offset,
|
|
verifiedSectors: offset,
|
|
coveredSectors: offset,
|
|
mismatchSector: offset.nullable(),
|
|
malformed: z.boolean(),
|
|
}),
|
|
});
|
|
|
|
/** NTFS $MFT record-header and resident file-name coverage facts. */
|
|
export const ntfsMftMetadataSchema = z.strictObject({
|
|
kind: z.literal("ntfs-mft"),
|
|
record_bytes: offset,
|
|
record_count: offset,
|
|
in_use_record_count: offset,
|
|
directory_record_count: offset,
|
|
extension_record_count: offset,
|
|
fixup_mismatch_count: offset,
|
|
fixup_unverified_record_count: offset,
|
|
truncated_tail_bytes: offset,
|
|
record_headers: z.array(ntfsRecordHeaderSchema).max(MFT_INLINE_HEADER_LIMIT),
|
|
omitted_record_headers: offset,
|
|
record_header_start_index: offset.exactOptional(),
|
|
next_record_header_index: offset.nullable().exactOptional(),
|
|
file_name_count: offset.exactOptional(),
|
|
file_name_incomplete_record_count: offset.exactOptional(),
|
|
attribute_incomplete_record_count: offset.exactOptional(),
|
|
});
|
|
|
|
/**
|
|
* U-Boot FIT root facts. Configuration references are declared names; a
|
|
* reference that names no image is reported as a limitation, not removed.
|
|
*/
|
|
export const fitMetadataSchema = z.strictObject({
|
|
kind: z.literal("fit"),
|
|
fdt_version: uint32,
|
|
fdt_totalsize: offset,
|
|
external_data_base: offset,
|
|
description: z.string().nullable(),
|
|
timestamp: uint32.nullable(),
|
|
address_cells: uint32.nullable(),
|
|
default_configuration: z.string().nullable(),
|
|
configurations: z.array(
|
|
z.strictObject({
|
|
name: z.string(),
|
|
description: z.string().nullable(),
|
|
kernel: z.array(z.string()),
|
|
firmware: z.array(z.string()),
|
|
fdt: z.array(z.string()),
|
|
ramdisk: z.array(z.string()),
|
|
loadables: z.array(z.string()),
|
|
signature_count: uint32,
|
|
signatures_unreadable: uint32,
|
|
unresolved_reference_count: uint32,
|
|
signature_algorithms: z.array(z.string()),
|
|
}),
|
|
),
|
|
});
|
|
export type FitMetadata = z.infer<typeof fitMetadataSchema>;
|
|
|
|
/**
|
|
* One /images node's facts. Null fields are absent unless unreadable_fields
|
|
* identifies an interpretation failure. Load and entry are declarations;
|
|
* stored bytes may be compressed or ciphered.
|
|
*/
|
|
export const fitImageMetadataSchema = z.strictObject({
|
|
kind: z.literal("fit-image"),
|
|
index: uint32,
|
|
name: z.string(),
|
|
description: z.string().nullable(),
|
|
type: z.string().nullable(),
|
|
arch: z.string().nullable(),
|
|
os: z.string().nullable(),
|
|
compression: z.string().nullable(),
|
|
load: z.string().nullable(),
|
|
entry: z.string().nullable(),
|
|
data_source: z.enum(["embedded", "data-offset", "data-position"]),
|
|
declared_offset: uint32.nullable(),
|
|
image_data: z.string().nullable(),
|
|
/** Presence is independent of whether the declared algorithm is readable. */
|
|
cipher_present: z.boolean(),
|
|
cipher_algo: z.string().nullable(),
|
|
unreadable_fields: z.array(
|
|
z.enum([
|
|
"description",
|
|
"type",
|
|
"arch",
|
|
"os",
|
|
"compression",
|
|
"load",
|
|
"entry",
|
|
"cipher_algo",
|
|
]),
|
|
),
|
|
hash_algorithms: z.array(z.string()),
|
|
hashes_verified: uint32,
|
|
hashes_unverified: uint32,
|
|
signature_count: uint32,
|
|
signatures_unreadable: uint32,
|
|
signature_algorithms: z.array(z.string()),
|
|
});
|
|
export type FitImageMetadata = z.infer<typeof fitImageMetadataSchema>;
|
|
|
|
export 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 });
|
|
|
|
/** Derived from `chip_id` through the cited table. */
|
|
const espChipFactsSchema = z.strictObject({
|
|
target: z.string(),
|
|
isa: z.enum(["xtensa", "riscv"]),
|
|
source: z.string(),
|
|
});
|
|
|
|
/**
|
|
* `esp_app_desc_t` fields, as ESP-IDF names them; strings stop at NUL. A field
|
|
* the file cuts off is null; `observed_length` says where the descriptor ends.
|
|
*/
|
|
export const espAppDescriptorSchema = z.strictObject({
|
|
kind: z.literal("app"),
|
|
offset,
|
|
/** Descriptor bytes the file holds, at most the 256 of `esp_app_desc_t`. */
|
|
observed_length: z.int().min(4).max(256),
|
|
secure_version: uint32.nullable(),
|
|
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.nullable(),
|
|
min_efuse_blk_rev_full: uint16.nullable(),
|
|
max_efuse_blk_rev_full: uint16.nullable(),
|
|
/**
|
|
* ESP-IDF's value: the MMU page size as a base-2 logarithm (16 is 64 KiB);
|
|
* 0 in images built before ESP-IDF v5.4.
|
|
*/
|
|
mmu_page_size: uint8.nullable(),
|
|
/**
|
|
* The byte ESP-IDF v6.1 names `spi_flash_mode`, as stored; REA does not
|
|
* interpret it. Releases that reserve this byte leave it zero.
|
|
*/
|
|
spi_flash_mode: uint8.nullable(),
|
|
});
|
|
|
|
/** `esp_bootloader_desc_t` fields, as ESP-IDF names them. */
|
|
export 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: espChipFactsSchema.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(),
|
|
/**
|
|
* Set when the file is a flash dump read from a later flash address: this
|
|
* image is not an app, its chip's ROM loads the bootloader from
|
|
* `flash_offset`, and a partition table sits at `table_file_offset`, where
|
|
* that origin puts the default `0x8000`. Reopen the file as
|
|
* `esp-flash-image` with that `flash_offset`. Null otherwise, and in the
|
|
* nested facts of a flash layout. Absent from Evidence recorded before REA
|
|
* reported it; its graph digest covers the record without the field.
|
|
*/
|
|
flash_dump: z
|
|
.strictObject({ flash_offset: uint32, table_file_offset: offset })
|
|
.nullable()
|
|
.exactOptional(),
|
|
});
|
|
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
|
|
>;
|
|
|
|
/** One partition table entry as declared, with ESP-IDF's type names. */
|
|
export const espFlashPartitionMetadataSchema = z.strictObject({
|
|
kind: z.literal("esp-flash-partition"),
|
|
/** Position among the table's partition entries. */
|
|
index: uint8,
|
|
/** The label up to its first NUL; null when it is not UTF-8. */
|
|
label: z.string().nullable(),
|
|
/** The label's bytes up to its first NUL in hex, when it is not UTF-8. */
|
|
label_hex: z
|
|
.string()
|
|
.regex(/^(?:[0-9a-f]{2}){1,16}$/u)
|
|
.nullable(),
|
|
type: uint8,
|
|
subtype: uint8,
|
|
/** `gen_esp32part.py` names; null for values it does not name. */
|
|
type_name: z.string().nullable(),
|
|
subtype_name: z.string().nullable(),
|
|
flash_address: uint32,
|
|
size: uint32,
|
|
encrypted: z.boolean(),
|
|
readonly: z.boolean(),
|
|
/** Flag bits other than `encrypted` and `readonly`, which ESP-IDF ignores. */
|
|
unknown_flags: uint32,
|
|
/**
|
|
* Observed in the bytes the dump holds, from the partition's start: the
|
|
* first byte that is not 0xFF and just past the last, null when every byte
|
|
* is erased, and how many bytes are not 0xFF. All null for a partition the
|
|
* dump holds none of.
|
|
*/
|
|
written_offset: uint32.nullable(),
|
|
written_end: uint32.nullable(),
|
|
written_bytes: uint32.nullable(),
|
|
});
|
|
|
|
/** ESP image facts for a region of a dump; offsets are relative to the region. */
|
|
export const espFlashImageFactsSchema = z.strictObject({
|
|
/** An ESP image, erased flash, or bytes that are not one, such as ciphertext. */
|
|
status: z.enum(["esp-image", "erased", "unrecognized"]),
|
|
image: espImageMetadataSchema.nullable(),
|
|
segments: z.array(espImageSegmentMetadataSchema),
|
|
signature_blocks: z.array(espImageSignatureBlockMetadataSchema),
|
|
coverage: z.enum(["complete", "unknown"]).nullable(),
|
|
limitations: z.array(z.string()),
|
|
});
|
|
|
|
const espOtaRecordSchema = z.strictObject({
|
|
index: z.union([z.literal(0), z.literal(1)]),
|
|
ota_seq: uint32,
|
|
seq_label: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
ota_state: uint32,
|
|
state_name: z
|
|
.enum(["new", "pending-verify", "valid", "invalid", "aborted", "undefined"])
|
|
.nullable(),
|
|
crc: uint32,
|
|
/** CRC-32 of `ota_seq` seeded with 0xFFFFFFFF, as the bootloader checks it. */
|
|
crc_matches: z.boolean(),
|
|
valid: z.boolean(),
|
|
});
|
|
|
|
/**
|
|
* An ESP flash dump: where it sits in flash, its bootloader, partition table,
|
|
* partitions and OTA data. Entries are the table's partitions; app and
|
|
* bootloader regions carry their ESP image facts here.
|
|
*/
|
|
export const espFlashImageMetadataSchema = z.strictObject({
|
|
kind: z.literal("esp-flash-image"),
|
|
/** Flash address of the file's first byte: 0 unless the caller gave one. */
|
|
flash_offset: z.strictObject({
|
|
value: uint32,
|
|
source: z.enum(["default", "caller"]),
|
|
}),
|
|
/** From the bootloader's `chip_id`; null without a located bootloader. */
|
|
chip: espChipFactsSchema.nullable(),
|
|
/**
|
|
* The image at its chip's ROM offset, through the partition table; `path`
|
|
* is its entry.
|
|
*/
|
|
bootloader: z
|
|
.strictObject({
|
|
path: z.string(),
|
|
flash_address: uint32,
|
|
region_size: offset,
|
|
facts: espFlashImageFactsSchema,
|
|
})
|
|
.nullable(),
|
|
partition_table: z.strictObject({
|
|
flash_address: uint32,
|
|
/** 0x8000, the caller's offset, or a scan for a verified MD5 (derived). */
|
|
method: z.enum(["default", "caller", "scan"]),
|
|
partition_count: uint8,
|
|
md5: z.strictObject({
|
|
status: z.enum(["valid", "invalid", "absent"]),
|
|
stored: z
|
|
.string()
|
|
.regex(/^[0-9a-f]{32}$/u)
|
|
.nullable(),
|
|
computed: z
|
|
.string()
|
|
.regex(/^[0-9a-f]{32}$/u)
|
|
.nullable(),
|
|
}),
|
|
}),
|
|
/**
|
|
* Declared partitions that begin before the dump's first byte, which have
|
|
* no entry. A partition past the dump's end is an entry with no bytes.
|
|
*/
|
|
partitions_outside_dump: z.array(espFlashPartitionMetadataSchema),
|
|
/** ESP image facts for each app partition the dump holds, by its entry. */
|
|
apps: z.array(
|
|
z.strictObject({
|
|
path: z.string(),
|
|
partition_index: uint8,
|
|
facts: espFlashImageFactsSchema,
|
|
}),
|
|
),
|
|
/** The otadata partition the bootloader uses: the last one declared. */
|
|
otadata: z
|
|
.strictObject({
|
|
partition_index: uint8,
|
|
/** The records the dump holds, at most one per sector. */
|
|
records: z.array(espOtaRecordSchema).max(2),
|
|
})
|
|
.nullable(),
|
|
/**
|
|
* The app the bootloader would select, derived for every table under the
|
|
* stated assumptions. `basis` names the branch: `otadata`,
|
|
* `factory-fallback` and `ota_0-fallback` from the otadata records,
|
|
* `no-otadata` for a table without otadata (the factory index), `none`
|
|
* for the bootloader's INVALID_INDEX, after which it resets, and
|
|
* `unknown` when the dump lacks a record the bootloader reads.
|
|
* `partition_index` is null when the table declares no partition for the
|
|
* selected slot or fallback.
|
|
*/
|
|
boot_selection: z.strictObject({
|
|
basis: z.enum([
|
|
"otadata",
|
|
"factory-fallback",
|
|
"ota_0-fallback",
|
|
"no-otadata",
|
|
"none",
|
|
"unknown",
|
|
]),
|
|
active_record: z.union([z.literal(0), z.literal(1)]).nullable(),
|
|
slot: uint8.nullable(),
|
|
partition_index: uint8.nullable(),
|
|
assumptions: z.array(z.string()),
|
|
/** Rollback, anti-rollback and load fallback decide it at boot. */
|
|
running_partition: z.literal("unknown"),
|
|
}),
|
|
/**
|
|
* Ranges outside every described region, each from a byte that is not
|
|
* erased flash to the end of its gap.
|
|
*/
|
|
undescribed: z.array(byteRange),
|
|
});
|
|
export type EspFlashImageMetadata = z.infer<typeof espFlashImageMetadataSchema>;
|
|
|
|
/**
|
|
* U-Boot legacy uImage header facts. Declared addresses are facts from the
|
|
* header, not proven entry points; CRCs are verified before inventory.
|
|
*/
|
|
export const uImageMetadataSchema = z.strictObject({
|
|
kind: z.literal("uimage"),
|
|
magic: z.literal(0x27051956),
|
|
timestamp: uint32,
|
|
timestamp_iso: z.string(),
|
|
payload_size: uint32,
|
|
load_address: uint32,
|
|
entry_point: uint32,
|
|
header_crc32: uint32,
|
|
data_crc32: uint32,
|
|
crc_verified: z.literal(true),
|
|
os: z.number().int().min(0).max(255),
|
|
os_name: z.string(),
|
|
arch: z.number().int().min(0).max(255),
|
|
arch_name: z.string(),
|
|
type: z.number().int().min(0).max(255),
|
|
type_name: z.string(),
|
|
compression: z.number().int().min(0).max(255),
|
|
compression_name: z.string(),
|
|
name: z.string().nullable(),
|
|
name_bytes_hex: z.string().regex(/^[0-9a-f]{64}$/u),
|
|
sub_image_count: uint32,
|
|
});
|
|
export type UImageMetadata = z.infer<typeof uImageMetadataSchema>;
|
|
|
|
/** One stored payload, or one multi-image sub-image, as stored bytes. */
|
|
export const uImageEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("uimage-entry"),
|
|
role: z.enum(["payload", "sub-image"]),
|
|
index: uint32,
|
|
});
|
|
export type UImageEntryMetadata = z.infer<typeof uImageEntryMetadataSchema>;
|
|
|
|
const bunHex = z
|
|
.string()
|
|
.regex(/^[0-9a-f]+$/u)
|
|
.nullable();
|
|
|
|
/**
|
|
* Container facts for one Bun standalone module graph. `flags` stays the raw
|
|
* u32 because Bun 1.3 and 1.4 reuse the low bits for different meanings.
|
|
*/
|
|
export const bunStandaloneMetadataSchema = z.strictObject({
|
|
kind: z.literal("bun-standalone"),
|
|
layout_revision: z.enum(["bun-1.3.3", "bun-1.3.9"]),
|
|
carrier: z.enum([
|
|
"mach-o-section",
|
|
"pe-section",
|
|
"elf-section",
|
|
"appended-trailer",
|
|
]),
|
|
payload_offset: offset,
|
|
payload_length: offset,
|
|
module_count: uint32,
|
|
entry_point_id: uint32,
|
|
entry_point_source_name: z.string().nullable(),
|
|
entry_point_logical_path: z.string().nullable(),
|
|
compile_exec_argv: z.string().nullable(),
|
|
compile_exec_argv_hex: bunHex,
|
|
flags: uint32,
|
|
});
|
|
export type BunStandaloneMetadata = z.infer<typeof bunStandaloneMetadataSchema>;
|
|
|
|
/** One stored region of a Bun module. Siblings repeat the module's source facts. */
|
|
export const bunStandaloneEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("bun-standalone-entry"),
|
|
index: uint32,
|
|
region: z.enum(["contents", "sourcemap", "bytecode", "module-info"]),
|
|
source_name: z.string().nullable(),
|
|
source_name_hex: bunHex,
|
|
encoding: uint32,
|
|
encoding_name: z.enum(["binary", "latin1", "utf8", "utf16"]).nullable(),
|
|
loader: uint32,
|
|
loader_name: z.string().nullable(),
|
|
module_format: uint32,
|
|
module_format_name: z.enum(["none", "esm", "cjs"]).nullable(),
|
|
side: uint32,
|
|
side_name: z.enum(["server", "client"]).nullable(),
|
|
entry_point: z.boolean(),
|
|
sourcemap_bytes: offset,
|
|
bytecode_bytes: offset,
|
|
module_info_bytes: offset,
|
|
bytecode_origin: z.string().nullable(),
|
|
bytecode_origin_hex: bunHex,
|
|
});
|
|
export type BunStandaloneEntryMetadata = z.infer<
|
|
typeof bunStandaloneEntryMetadataSchema
|
|
>;
|
|
|
|
const denoListingSchema = z.enum(["listed", "unsupported"]);
|
|
|
|
const denoEnvVarSchema = z.strictObject({
|
|
name: z.string(),
|
|
value: z.string(),
|
|
value_byte_length: offset,
|
|
});
|
|
|
|
/**
|
|
* Container facts for one Deno compile section. The metadata JSON has no Deno
|
|
* version field. Environment-file values and CA bytes are retained as local evidence.
|
|
*/
|
|
export const denoStandaloneMetadataSchema = z.strictObject({
|
|
kind: z.literal("deno-standalone"),
|
|
carrier: z.enum(["mach-o", "elf", "pe"]),
|
|
embedding: z.enum([
|
|
"mach-o-section",
|
|
"mach-o-intel-trailer",
|
|
"elf-note",
|
|
"pe-resource",
|
|
]),
|
|
section_offset: offset,
|
|
section_length: offset,
|
|
entrypoint_key: z.string(),
|
|
argv: z.array(z.string()).nullable(),
|
|
v8_flags: z.array(z.string()).nullable(),
|
|
permissions: jsonRecordSchema(jsonValueSchema).nullable(),
|
|
unstable_config: jsonRecordSchema(jsonValueSchema).nullable(),
|
|
app_name: z.string().nullable(),
|
|
self_extracting: z.string().nullable(),
|
|
npm_snapshot_byte_length: offset,
|
|
env_var_count: uint32.nullable(),
|
|
env_vars: z.array(denoEnvVarSchema).nullable(),
|
|
ca_data: z.array(z.number().int().min(0).max(255)).nullable(),
|
|
ca_data_byte_length: offset.nullable(),
|
|
specifier_count: uint32,
|
|
specifier_byte_length: offset,
|
|
specifier_listing: denoListingSchema,
|
|
redirect_count: uint32,
|
|
redirect_byte_length: offset,
|
|
redirect_listing: denoListingSchema,
|
|
remote_module_count: uint32,
|
|
remote_module_byte_length: offset,
|
|
remote_module_listing: denoListingSchema,
|
|
vfs_header_byte_length: offset,
|
|
vfs_file_data_byte_length: offset,
|
|
vfs_listing: denoListingSchema,
|
|
vfs_case_sensitivity: z.enum(["sensitive", "insensitive"]).nullable(),
|
|
});
|
|
export type DenoStandaloneMetadata = z.infer<
|
|
typeof denoStandaloneMetadataSchema
|
|
>;
|
|
|
|
/** One stored region of a Deno virtual file, symlink, or remote module. */
|
|
export const denoStandaloneEntryMetadataSchema = z.strictObject({
|
|
kind: z.literal("deno-standalone-entry"),
|
|
role: z.enum([
|
|
"vfs-file",
|
|
"vfs-transpiled",
|
|
"vfs-cjs-export-analysis",
|
|
"vfs-source-map",
|
|
"vfs-symlink",
|
|
"remote-module",
|
|
"remote-transpiled",
|
|
"remote-source-map",
|
|
"remote-cjs-export-analysis",
|
|
]),
|
|
index: uint32,
|
|
source_path: z.string().nullable(),
|
|
entry_point: z.boolean(),
|
|
executable: z.boolean(),
|
|
is_valid_utf8: z.boolean().nullable(),
|
|
symlink_dest_parts: z.array(z.string()).nullable(),
|
|
media_type: z.number().int().min(0).max(255).nullable(),
|
|
media_type_name: z.string().nullable(),
|
|
});
|
|
export type DenoStandaloneEntryMetadata = z.infer<
|
|
typeof denoStandaloneEntryMetadataSchema
|
|
>;
|
|
|
|
export const artifactFormatMetadataSchema = z.discriminatedUnion("kind", [
|
|
godotPckMetadataSchema,
|
|
godotPckEntryMetadataSchema,
|
|
unityFsMetadataSchema,
|
|
unityFsEntryMetadataSchema,
|
|
wxapkgMetadataSchema,
|
|
wxapkgEntryMetadataSchema,
|
|
pyInstallerMetadataSchema,
|
|
pyInstallerEntryMetadataSchema,
|
|
dotnetBundleEntryMetadataSchema,
|
|
bunStandaloneMetadataSchema,
|
|
bunStandaloneEntryMetadataSchema,
|
|
denoStandaloneMetadataSchema,
|
|
denoStandaloneEntryMetadataSchema,
|
|
nodeSeaMetadataSchema,
|
|
nodeSeaEntryMetadataSchema,
|
|
hexRecordsMetadataSchema,
|
|
hexRecordRegionMetadataSchema,
|
|
uImageMetadataSchema,
|
|
uImageEntryMetadataSchema,
|
|
espImageMetadataSchema,
|
|
espImageSegmentMetadataSchema,
|
|
espImageSignatureBlockMetadataSchema,
|
|
uf2MetadataSchema,
|
|
uf2RegionMetadataSchema,
|
|
ntfsMftMetadataSchema,
|
|
dfuSeMetadataSchema,
|
|
dfuSeElementMetadataSchema,
|
|
espFlashImageMetadataSchema,
|
|
espFlashPartitionMetadataSchema,
|
|
fitMetadataSchema,
|
|
fitImageMetadataSchema,
|
|
]);
|
|
export type ArtifactFormatMetadata = z.infer<
|
|
typeof artifactFormatMetadataSchema
|
|
>;
|
|
|
|
type FormatMetadataByKind = {
|
|
readonly [Metadata in ArtifactFormatMetadata as Metadata["kind"]]: Metadata;
|
|
};
|
|
type FormatMetadataKind = keyof FormatMetadataByKind;
|
|
|
|
/**
|
|
* What each kind declares beyond its own bytes, which artifact comparison
|
|
* compares, or why comparison uses none of its facts. Every kind needs an
|
|
* entry, so a new kind does not compile without this decision; a new field
|
|
* of an existing kind still needs a review of its projection. For a node-less
|
|
* metadata record, the projection is all of its format metadata that
|
|
* comparison checks.
|
|
*/
|
|
const declarationsByKind: {
|
|
readonly [Kind in FormatMetadataKind]:
|
|
| {
|
|
readonly declaration: (
|
|
metadata: FormatMetadataByKind[Kind],
|
|
) => FormatMetadataDeclaration["facts"];
|
|
/** Projected fields the producer could not interpret. */
|
|
readonly unreadable?: (
|
|
metadata: FormatMetadataByKind[Kind],
|
|
) => FormatMetadataDeclaration["unreadable"];
|
|
}
|
|
| { readonly notCompared: string };
|
|
} = {
|
|
"godot-pck": {
|
|
notCompared:
|
|
"Its fields come from the pack's own bytes, which content comparison covers.",
|
|
},
|
|
"godot-pck-entry": {
|
|
notCompared:
|
|
"index and declared_offset are layout; flags and md5_status repeat occurrence facts; source_path spells Godot's lookup path in a version-dependent way. docs/godot-pck.md gives the evidence.",
|
|
},
|
|
unityfs: {
|
|
notCompared:
|
|
"Its fields come from the bundle's own bytes, which content comparison covers.",
|
|
},
|
|
/** What the directory declares for a member: its source path and raw node flags. Index and offset are layout. */
|
|
"unityfs-entry": {
|
|
declaration: (metadata) => ({
|
|
source_path: metadata.source_path,
|
|
flags: metadata.flags,
|
|
}),
|
|
},
|
|
wxapkg: {
|
|
notCompared:
|
|
"Its fields come from the package's own bytes, which content comparison covers.",
|
|
},
|
|
"wxapkg-entry": {
|
|
notCompared:
|
|
"index is layout. source_path keeps the declared name, including a producer root prefix the occurrence path strips.",
|
|
},
|
|
pyinstaller: {
|
|
notCompared:
|
|
"Its fields come from the archive's own bytes, which content comparison covers.",
|
|
},
|
|
/** Declaration identity that is stable across archive layout changes. */
|
|
"pyinstaller-entry": {
|
|
declaration: (metadata) => ({
|
|
container: metadata.container,
|
|
container_name: metadata.container_name,
|
|
source_name: metadata.source_name,
|
|
type_code: metadata.type_code,
|
|
}),
|
|
},
|
|
"dotnet-bundle-entry": {
|
|
notCompared:
|
|
"Its fields come from the host's bundle manifest, whose bytes the host's content comparison covers.",
|
|
},
|
|
"bun-standalone": {
|
|
notCompared:
|
|
"Its fields come from the carrier's own bytes, which content comparison covers.",
|
|
},
|
|
"bun-standalone-entry": {
|
|
notCompared:
|
|
"Its fields come from the carrier's module graph, whose bytes the carrier's content comparison covers.",
|
|
},
|
|
"deno-standalone": {
|
|
notCompared:
|
|
"Its fields come from the carrier's own bytes, which content comparison covers.",
|
|
},
|
|
"deno-standalone-entry": {
|
|
notCompared:
|
|
"Its fields come from the carrier's Deno section, whose bytes the carrier's content comparison covers.",
|
|
},
|
|
"node-sea": {
|
|
notCompared:
|
|
"Its fields come from the carrier's own bytes, which content comparison covers.",
|
|
},
|
|
/** Runtime member identity, independent of blob layout and directory order. */
|
|
"node-sea-entry": {
|
|
declaration: (metadata) => ({
|
|
role: metadata.role,
|
|
source_path: metadata.source_path,
|
|
}),
|
|
},
|
|
"hex-records": {
|
|
notCompared:
|
|
"Its fields come from the file's own records, which content comparison covers.",
|
|
},
|
|
"hex-record-region": {
|
|
notCompared:
|
|
"start_address and end_address restate the region's path, and length follows from them; record and line counts are layout.",
|
|
},
|
|
uimage: {
|
|
notCompared:
|
|
"Its header facts come from the image's own bytes, which content comparison covers.",
|
|
},
|
|
"uimage-entry": {
|
|
notCompared:
|
|
"role and index restate the entry's path; its stored bytes are compared by content.",
|
|
},
|
|
"esp-image": {
|
|
notCompared:
|
|
"Its fields come from the image's own bytes, which content comparison covers.",
|
|
},
|
|
/**
|
|
* What a segment header declares: where its bytes load and how many there
|
|
* are. Header offsets are layout, and the classification is derived.
|
|
*/
|
|
"esp-image-segment": {
|
|
declaration: (metadata) => ({
|
|
load_addr: metadata.load_addr,
|
|
data_len: metadata.data_len,
|
|
}),
|
|
},
|
|
"esp-image-signature-block": {
|
|
notCompared:
|
|
"Its fields come from the block's own bytes, which content comparison covers, and image_digest_matches follows the image's content.",
|
|
},
|
|
uf2: {
|
|
notCompared:
|
|
"Its counts come from the file's own blocks, which content comparison covers, and family_name_registry names REA's label registry.",
|
|
},
|
|
"uf2-region": {
|
|
notCompared:
|
|
"Its familyID and addresses are in its path, family_name is REA's label for the familyID, and its block counts and positions are layout; content comparison covers its bytes.",
|
|
},
|
|
"ntfs-mft": {
|
|
notCompared:
|
|
"Its fields come from the file's own FILE record headers and resident file-name attributes, which content comparison covers.",
|
|
},
|
|
dfuse: {
|
|
notCompared:
|
|
"Its fields come from the file's own prefix, target headers and suffix, which content comparison covers.",
|
|
},
|
|
/** The device memory an element targets, which its path and bytes do not show. */
|
|
"dfuse-element": {
|
|
declaration: (metadata) => ({
|
|
alternate_setting: metadata.alternate_setting,
|
|
target_name: metadata.target_name,
|
|
}),
|
|
},
|
|
"esp-flash-image": {
|
|
notCompared:
|
|
"Its fields come from the dump's bytes and from the caller's flash_offset and partition_table_offset, which are target identity; each partition's range, compared by content, reflects them.",
|
|
},
|
|
/**
|
|
* What a partition entry declares; its index is layout, and the written
|
|
* fields are observed from its bytes, which content comparison covers.
|
|
*/
|
|
"esp-flash-partition": {
|
|
declaration: (metadata) => ({
|
|
label: metadata.label,
|
|
label_hex: metadata.label_hex,
|
|
type: metadata.type,
|
|
subtype: metadata.subtype,
|
|
flash_address: metadata.flash_address,
|
|
size: metadata.size,
|
|
encrypted: metadata.encrypted,
|
|
readonly: metadata.readonly,
|
|
unknown_flags: metadata.unknown_flags,
|
|
}),
|
|
},
|
|
fit: {
|
|
notCompared:
|
|
"Its fields come from the FIT's own bytes, which content comparison covers.",
|
|
},
|
|
/**
|
|
* Payload interpretation and sharing declarations, independent of FIT
|
|
* placement: the image's name, index and data position are layout.
|
|
*/
|
|
"fit-image": {
|
|
declaration: (metadata) => ({
|
|
type: metadata.type,
|
|
arch: metadata.arch,
|
|
os: metadata.os,
|
|
compression: metadata.compression,
|
|
load: metadata.load,
|
|
entry: metadata.entry,
|
|
image_data: metadata.image_data,
|
|
cipher_present: metadata.cipher_present,
|
|
cipher_algo: metadata.cipher_algo,
|
|
}),
|
|
unreadable: (metadata) => metadata.unreadable_fields,
|
|
},
|
|
};
|
|
|
|
/** Kinds whose declarations artifact comparison compares, in table order. */
|
|
export const COMPARED_FORMAT_METADATA_KINDS = Object.entries(
|
|
declarationsByKind,
|
|
).flatMap(([kind, entry]) => ("declaration" in entry ? [kind] : []));
|
|
|
|
/** A declaration that artifact comparison compares within its kind. */
|
|
export interface FormatMetadataDeclaration {
|
|
readonly kind: FormatMetadataKind;
|
|
readonly facts: Readonly<Record<string, unknown>>;
|
|
/** Facts whose value is unknown, which compare as neither equal nor changed. */
|
|
readonly unreadable: readonly string[];
|
|
}
|
|
|
|
// Generic in the kind, so each projection receives its own metadata type.
|
|
const declarationOf = <Kind extends FormatMetadataKind>(
|
|
kind: Kind,
|
|
metadata: FormatMetadataByKind[Kind],
|
|
): FormatMetadataDeclaration | undefined => {
|
|
const entry = declarationsByKind[kind];
|
|
return "declaration" in entry
|
|
? {
|
|
kind,
|
|
facts: entry.declaration(metadata),
|
|
unreadable: entry.unreadable?.(metadata) ?? [],
|
|
}
|
|
: undefined;
|
|
};
|
|
|
|
/** The compared declaration, or undefined when comparison uses none. */
|
|
export const formatMetadataDeclaration = (
|
|
metadata: ArtifactFormatMetadata | undefined,
|
|
): FormatMetadataDeclaration | undefined =>
|
|
metadata === undefined ? undefined : declarationOf(metadata.kind, metadata);
|