1219 lines
41 KiB
Python
1219 lines
41 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate and verify assertion-driven G6 chunk/multidevice fixtures."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import math
|
|
import os
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
import tempfile
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
from erofs_fixture import ErofsImage, SUPER
|
|
|
|
|
|
FEATURE_INCOMPAT_CHUNKED_FILE = 0x00000004
|
|
FEATURE_INCOMPAT_DEVICE_TABLE = 0x00000008
|
|
FEATURE_INCOMPAT_48BIT = 0x00000080
|
|
CHUNK_FORMAT_BLOCK_BITS_MASK = 0x001F
|
|
CHUNK_FORMAT_INDEXES = 0x0020
|
|
CHUNK_FORMAT_48BIT = 0x0040
|
|
DEVICE_SLOT_SIZE = 128
|
|
DEVICE_SLOT_FIELDS = 64
|
|
BLOCK_SIZE = 4096
|
|
|
|
CHUNK_UUID = "67360006-0093-0094-0095-000000000101"
|
|
FRAGMENT_UUID = "67360098-0000-0000-0000-000000000098"
|
|
PCLUSTER_UUID = "67360094-0101-0000-0000-000000000094"
|
|
|
|
CHUNK_FILES = {
|
|
"tc006.bin": 24,
|
|
"cross-device.bin": 96,
|
|
"indexed.bin": 40,
|
|
"cold-slot2.bin": 32,
|
|
"unified-address.bin": 32,
|
|
}
|
|
|
|
|
|
class FixtureError(RuntimeError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ChunkEntry:
|
|
offset: int
|
|
start_block_high: int
|
|
device_id: int
|
|
start_block_low: int
|
|
|
|
@property
|
|
def start_block(self) -> int:
|
|
return self.start_block_low | (self.start_block_high << 32)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ChunkLayout:
|
|
path: str
|
|
nid: int
|
|
inode_offset: int
|
|
format_offset: int
|
|
chunk_format: int
|
|
chunk_bits: int
|
|
chunk_size: int
|
|
entry_size: int
|
|
index_base: int
|
|
entries: tuple[ChunkEntry, ...]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DeviceSlot:
|
|
blocks: int
|
|
uniaddr: int
|
|
|
|
|
|
@dataclass
|
|
class MultiFixture:
|
|
primary: ErofsImage
|
|
providers: list[bytearray]
|
|
slots: list[DeviceSlot]
|
|
|
|
|
|
def align_up(value: int, alignment: int) -> int:
|
|
return (value + alignment - 1) & ~(alignment - 1)
|
|
|
|
|
|
def sha256_bytes(data: bytes | bytearray) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def sha256_file(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def deterministic_block(path: str, block: int) -> bytes:
|
|
seed = f"repo22-g6:{path}:{block}".encode("ascii")
|
|
return b"".join(
|
|
hashlib.sha256(seed + index.to_bytes(4, "little")).digest()
|
|
for index in range(128)
|
|
)[:BLOCK_SIZE]
|
|
|
|
|
|
def run(
|
|
command: list[str], *, stdout: bool = False, cwd: Path | None = None
|
|
) -> str:
|
|
result = subprocess.run(
|
|
command,
|
|
check=True,
|
|
cwd=cwd,
|
|
stdout=subprocess.PIPE if stdout else subprocess.DEVNULL,
|
|
stderr=subprocess.STDOUT if stdout else None,
|
|
text=True,
|
|
)
|
|
return result.stdout if stdout else ""
|
|
|
|
|
|
def tool_version(tool: str) -> str:
|
|
output = run([tool, "-V"], stdout=True).strip().splitlines()
|
|
if not output:
|
|
raise FixtureError(f"{tool} returned no version")
|
|
return output[0]
|
|
|
|
|
|
def write_chunk_sources(root: Path) -> None:
|
|
root.mkdir(parents=True)
|
|
for name, blocks in CHUNK_FILES.items():
|
|
with (root / name).open("wb") as stream:
|
|
for block in range(blocks):
|
|
stream.write(deterministic_block(name, block))
|
|
|
|
|
|
def write_fragment_source(root: Path) -> None:
|
|
root.mkdir(parents=True)
|
|
seed = deterministic_block("fragment.dat", 0)
|
|
data = (seed * math.ceil(100000 / len(seed)))[:100000]
|
|
(root / "fragment.dat").write_bytes(data)
|
|
|
|
|
|
def write_pcluster_source(root: Path) -> None:
|
|
root.mkdir(parents=True)
|
|
seed = b"".join(
|
|
hashlib.sha256(f"repo22-g6:pcluster:{index}".encode("ascii")).digest()
|
|
for index in range(188)
|
|
)[:6000]
|
|
data = (seed * math.ceil(131072 / len(seed)))[:131072]
|
|
(root / "external-pcluster.bin").write_bytes(data)
|
|
|
|
|
|
def make_image(
|
|
mkfs: str,
|
|
output: Path,
|
|
source: Path,
|
|
uuid: str,
|
|
options: list[str],
|
|
) -> None:
|
|
run(
|
|
[
|
|
mkfs,
|
|
"-T0",
|
|
"--all-time",
|
|
"--all-root",
|
|
"--workers=1",
|
|
f"-U{uuid}",
|
|
*options,
|
|
str(output),
|
|
str(source),
|
|
]
|
|
)
|
|
|
|
|
|
def chunk_layout(image: ErofsImage, path: str) -> ChunkLayout:
|
|
_, entry = image.resolve_root_entry(path)
|
|
inode = image.inode(entry.nid)
|
|
if inode.layout != 4:
|
|
raise FixtureError(f"{path}: expected chunk layout, got {inode.layout}")
|
|
chunk_format, reserved = struct.unpack_from(
|
|
"<HH", image.data, inode.offset + 16
|
|
)
|
|
if reserved != 0:
|
|
raise FixtureError(f"{path}: chunk reserved field is nonzero")
|
|
chunk_bits = image.block_bits + (
|
|
chunk_format & CHUNK_FORMAT_BLOCK_BITS_MASK
|
|
)
|
|
chunk_size = 1 << chunk_bits
|
|
count = math.ceil(inode.size / chunk_size)
|
|
entry_size = 8 if chunk_format & CHUNK_FORMAT_INDEXES else 4
|
|
index_base = align_up(
|
|
inode.offset + inode.inode_size + inode.xattr_size, entry_size
|
|
)
|
|
if index_base + count * entry_size > image.blocks * image.block_size:
|
|
raise FixtureError(f"{path}: chunk index array exceeds declared image")
|
|
entries = []
|
|
for index in range(count):
|
|
offset = index_base + index * entry_size
|
|
if entry_size == 8:
|
|
high, device_id, low = struct.unpack_from(
|
|
"<HHI", image.data, offset
|
|
)
|
|
else:
|
|
high, device_id = 0, 0
|
|
low = struct.unpack_from("<I", image.data, offset)[0]
|
|
entries.append(ChunkEntry(offset, high, device_id, low))
|
|
return ChunkLayout(
|
|
path=path,
|
|
nid=inode.nid,
|
|
inode_offset=inode.offset,
|
|
format_offset=inode.offset + 16,
|
|
chunk_format=chunk_format,
|
|
chunk_bits=chunk_bits,
|
|
chunk_size=chunk_size,
|
|
entry_size=entry_size,
|
|
index_base=index_base,
|
|
entries=tuple(entries),
|
|
)
|
|
|
|
|
|
def assert_chunk_baseline(image: ErofsImage, paths: list[str]) -> None:
|
|
for path in paths:
|
|
layout = chunk_layout(image, path)
|
|
if layout.entry_size != 8:
|
|
raise FixtureError(f"{path}: blob baseline has no 8-byte indexes")
|
|
minimum = 3 if path in ("/cross-device.bin", "/indexed.bin") else 1
|
|
if len(layout.entries) < minimum:
|
|
raise FixtureError(
|
|
f"{path}: need at least {minimum} chunk indexes"
|
|
)
|
|
for entry in layout.entries:
|
|
if entry.start_block_high != 0 or entry.device_id != 1:
|
|
raise FixtureError(f"{path}: unexpected mkfs chunk index {entry}")
|
|
|
|
|
|
def patch_chunk_entry(
|
|
image: ErofsImage,
|
|
entry: ChunkEntry,
|
|
*,
|
|
expected: tuple[int, int, int],
|
|
replacement: tuple[int, int, int],
|
|
) -> None:
|
|
current = struct.unpack_from("<HHI", image.data, entry.offset)
|
|
if current != expected:
|
|
raise FixtureError(
|
|
f"chunk index at {entry.offset}: expected {expected}, got {current}"
|
|
)
|
|
struct.pack_into("<HHI", image.data, entry.offset, *replacement)
|
|
|
|
|
|
def set_chunk_48bit(image: ErofsImage, paths: list[str]) -> None:
|
|
for path in paths:
|
|
layout = chunk_layout(image, path)
|
|
if layout.entry_size != 8:
|
|
raise FixtureError(f"{path}: 48-bit conversion requires indexes")
|
|
expected = layout.chunk_format
|
|
if expected & CHUNK_FORMAT_48BIT:
|
|
raise FixtureError(f"{path}: already has 48-bit indexes")
|
|
current = image.u16(layout.format_offset)
|
|
if current != expected:
|
|
raise FixtureError(f"{path}: chunk format changed before patch")
|
|
image.put_u16(layout.format_offset, expected | CHUNK_FORMAT_48BIT)
|
|
|
|
|
|
def enable_48bit(image: ErofsImage) -> None:
|
|
if image.feature_incompat & FEATURE_INCOMPAT_48BIT:
|
|
raise FixtureError("image already has 48-bit feature")
|
|
root_nid = image.root_nid
|
|
image.put_u32(
|
|
SUPER + 80, image.feature_incompat | FEATURE_INCOMPAT_48BIT
|
|
)
|
|
image.put_u16(SUPER + 14, 0)
|
|
image.put_u64(SUPER + 112, root_nid)
|
|
|
|
|
|
def decode_slots(image: ErofsImage) -> list[DeviceSlot]:
|
|
extra, slot_offset = struct.unpack_from("<HH", image.data, SUPER + 86)
|
|
table_offset = slot_offset * DEVICE_SLOT_SIZE
|
|
if table_offset + extra * DEVICE_SLOT_SIZE > len(image.data):
|
|
raise FixtureError("device table lies outside provider bytes")
|
|
slots = []
|
|
for index in range(extra):
|
|
offset = table_offset + index * DEVICE_SLOT_SIZE + DEVICE_SLOT_FIELDS
|
|
blocks_low, uniaddr_low, blocks_high, uniaddr_high = struct.unpack_from(
|
|
"<IIHH", image.data, offset
|
|
)
|
|
if image.feature_incompat & FEATURE_INCOMPAT_48BIT:
|
|
blocks = blocks_low | (blocks_high << 32)
|
|
uniaddr = uniaddr_low | (uniaddr_high << 32)
|
|
else:
|
|
blocks = blocks_low
|
|
uniaddr = uniaddr_low
|
|
slots.append(DeviceSlot(blocks, uniaddr))
|
|
return slots
|
|
|
|
|
|
def write_device_table(
|
|
image: ErofsImage,
|
|
slots: list[DeviceSlot],
|
|
*,
|
|
table_offset: int = BLOCK_SIZE,
|
|
primary_blocks: int = 2,
|
|
) -> None:
|
|
if not slots:
|
|
raise FixtureError("device table needs at least one slot")
|
|
if table_offset % DEVICE_SLOT_SIZE != 0:
|
|
raise FixtureError("device table is not 128-byte aligned")
|
|
required = max(
|
|
primary_blocks * image.block_size,
|
|
table_offset + len(slots) * DEVICE_SLOT_SIZE,
|
|
)
|
|
if len(image.data) > required:
|
|
raise FixtureError("primary metadata exceeds requested primary size")
|
|
image.data.extend(bytes(required - len(image.data)))
|
|
image.put_u32(SUPER + 36, primary_blocks)
|
|
image.put_u32(
|
|
SUPER + 80, image.feature_incompat | FEATURE_INCOMPAT_DEVICE_TABLE
|
|
)
|
|
image.put_u16(SUPER + 86, len(slots))
|
|
image.put_u16(SUPER + 88, table_offset // DEVICE_SLOT_SIZE)
|
|
image.data[
|
|
table_offset : table_offset + len(slots) * DEVICE_SLOT_SIZE
|
|
] = bytes(len(slots) * DEVICE_SLOT_SIZE)
|
|
for index, slot in enumerate(slots):
|
|
if slot.blocks <= 0 or slot.blocks >= 1 << 48:
|
|
raise FixtureError(f"slot {index + 1}: invalid blocks {slot.blocks}")
|
|
if slot.uniaddr < 0 or slot.uniaddr >= 1 << 48:
|
|
raise FixtureError(f"slot {index + 1}: invalid uniaddr {slot.uniaddr}")
|
|
offset = table_offset + index * DEVICE_SLOT_SIZE
|
|
tag = f"repo22-g6-slot-{index + 1}".encode("ascii")
|
|
image.data[offset : offset + len(tag)] = tag
|
|
struct.pack_into(
|
|
"<IIHH",
|
|
image.data,
|
|
offset + DEVICE_SLOT_FIELDS,
|
|
slot.blocks & 0xFFFFFFFF,
|
|
slot.uniaddr & 0xFFFFFFFF,
|
|
slot.blocks >> 32,
|
|
slot.uniaddr >> 32,
|
|
)
|
|
|
|
|
|
def finalize_image(image: ErofsImage, path: Path) -> None:
|
|
image.update_checksum()
|
|
image.validate_superblock()
|
|
image.save(path)
|
|
|
|
|
|
def build_single_indexed(
|
|
baseline: ErofsImage, blob: bytes, paths: list[str]
|
|
) -> ErofsImage:
|
|
image = baseline.clone()
|
|
if len(image.data) != BLOCK_SIZE or len(blob) % BLOCK_SIZE != 0:
|
|
raise FixtureError("single indexed folding requires aligned providers")
|
|
for path in paths:
|
|
layout = chunk_layout(image, path)
|
|
for entry in layout.entries:
|
|
patch_chunk_entry(
|
|
image,
|
|
entry,
|
|
expected=(0, 1, entry.start_block_low),
|
|
replacement=(0, 0, entry.start_block_low + 1),
|
|
)
|
|
image.data.extend(blob)
|
|
image.put_u32(SUPER + 36, len(image.data) // BLOCK_SIZE)
|
|
image.put_u32(
|
|
SUPER + 80, image.feature_incompat & ~FEATURE_INCOMPAT_DEVICE_TABLE
|
|
)
|
|
image.put_u16(SUPER + 86, 0)
|
|
image.put_u16(SUPER + 88, 0)
|
|
return image
|
|
|
|
|
|
def build_multislot(
|
|
baseline: ErofsImage,
|
|
blob: bytes,
|
|
paths: list[str],
|
|
slot_count: int,
|
|
) -> MultiFixture:
|
|
image = baseline.clone()
|
|
providers = [bytearray() for _ in range(slot_count)]
|
|
for path in paths:
|
|
layout = chunk_layout(image, path)
|
|
blocks_per_chunk = layout.chunk_size // BLOCK_SIZE
|
|
for index, entry in enumerate(layout.entries):
|
|
if entry.start_block_high != 0 or entry.device_id != 1:
|
|
raise FixtureError(f"{path}: unexpected source index {entry}")
|
|
source_start = entry.start_block * BLOCK_SIZE
|
|
source_end = source_start + blocks_per_chunk * BLOCK_SIZE
|
|
if source_end > len(blob):
|
|
raise FixtureError(f"{path}: source chunk exceeds blob provider")
|
|
slot = 1 if path == "/cold-slot2.bin" and slot_count >= 2 else (
|
|
index % slot_count
|
|
)
|
|
local_block = len(providers[slot]) // BLOCK_SIZE
|
|
providers[slot].extend(blob[source_start:source_end])
|
|
patch_chunk_entry(
|
|
image,
|
|
entry,
|
|
expected=(0, 1, entry.start_block_low),
|
|
replacement=(0, slot + 1, local_block),
|
|
)
|
|
for index, provider in enumerate(providers):
|
|
provider.extend(bytes((index + 1) * BLOCK_SIZE))
|
|
slots = []
|
|
uniaddr = 2
|
|
for provider in providers:
|
|
blocks = len(provider) // BLOCK_SIZE
|
|
slots.append(DeviceSlot(blocks, uniaddr))
|
|
uniaddr += blocks
|
|
write_device_table(image, slots)
|
|
return MultiFixture(image, providers, slots)
|
|
|
|
|
|
def clone_multifixture(fixture: MultiFixture) -> MultiFixture:
|
|
return MultiFixture(
|
|
fixture.primary.clone(),
|
|
[bytearray(provider) for provider in fixture.providers],
|
|
list(fixture.slots),
|
|
)
|
|
|
|
|
|
def rewrite_slots(
|
|
fixture: MultiFixture,
|
|
slots: list[DeviceSlot],
|
|
*,
|
|
table_offset: int = BLOCK_SIZE,
|
|
) -> None:
|
|
fixture.slots = slots
|
|
write_device_table(fixture.primary, slots, table_offset=table_offset)
|
|
|
|
|
|
def build_flatdev(fixture: MultiFixture) -> bytes:
|
|
end_block = max(
|
|
[fixture.primary.blocks]
|
|
+ [slot.uniaddr + slot.blocks for slot in fixture.slots]
|
|
)
|
|
data = bytearray(end_block * BLOCK_SIZE)
|
|
data[: len(fixture.primary.data)] = fixture.primary.data
|
|
for slot, provider in zip(fixture.slots, fixture.providers, strict=True):
|
|
if len(provider) != slot.blocks * BLOCK_SIZE:
|
|
raise FixtureError("provider length does not match slot declaration")
|
|
start = slot.uniaddr * BLOCK_SIZE
|
|
data[start : start + len(provider)] = provider
|
|
return bytes(data)
|
|
|
|
|
|
def first_entry(image: ErofsImage, path: str, index: int = 0) -> ChunkEntry:
|
|
layout = chunk_layout(image, path)
|
|
try:
|
|
return layout.entries[index]
|
|
except IndexError as error:
|
|
raise FixtureError(f"{path}: missing chunk index {index}") from error
|
|
|
|
|
|
def patch_unified_entry(
|
|
fixture: MultiFixture,
|
|
path: str,
|
|
*,
|
|
index: int = 0,
|
|
) -> None:
|
|
entry = first_entry(fixture.primary, path, index)
|
|
if entry.device_id < 1 or entry.device_id > len(fixture.slots):
|
|
raise FixtureError(f"{path}: source index has no external slot")
|
|
slot = fixture.slots[entry.device_id - 1]
|
|
global_block = slot.uniaddr + entry.start_block
|
|
patch_chunk_entry(
|
|
fixture.primary,
|
|
entry,
|
|
expected=(entry.start_block_high, entry.device_id, entry.start_block_low),
|
|
replacement=(global_block >> 32, 0, global_block & 0xFFFFFFFF),
|
|
)
|
|
|
|
|
|
def patch_table_field(
|
|
image: ErofsImage,
|
|
slot: int,
|
|
field_offset: int,
|
|
fmt: str,
|
|
value: int,
|
|
) -> None:
|
|
_, slot_offset = struct.unpack_from("<HH", image.data, SUPER + 86)
|
|
offset = slot_offset * DEVICE_SLOT_SIZE + (slot - 1) * DEVICE_SLOT_SIZE
|
|
offset += DEVICE_SLOT_FIELDS + field_offset
|
|
struct.pack_into(fmt, image.data, offset, value)
|
|
|
|
|
|
def make_pcluster_external(lz4_image: ErofsImage) -> tuple[MultiFixture, int]:
|
|
_, entry = lz4_image.resolve_root_entry("/external-pcluster.bin")
|
|
inode = lz4_image.inode(entry.nid)
|
|
if inode.layout != 1 or inode.size != 131072:
|
|
raise FixtureError("LZ4 source is not a legacy full-index inode")
|
|
header = align_up(inode.offset + inode.inode_size + inode.xattr_size, 8)
|
|
index_offset = header + 16
|
|
advise, cluster_offset, pblk = struct.unpack_from(
|
|
"<HHI", lz4_image.data, index_offset
|
|
)
|
|
if advise & 3 not in (1, 3) or cluster_offset != 0 or pblk != 1:
|
|
raise FixtureError(
|
|
f"unexpected LZ4 HEAD index {(advise, cluster_offset, pblk)}"
|
|
)
|
|
if len(lz4_image.data) != 3 * BLOCK_SIZE:
|
|
raise FixtureError("LZ4 source does not contain exactly two data blocks")
|
|
provider = bytearray(lz4_image.data[BLOCK_SIZE : 3 * BLOCK_SIZE])
|
|
image = lz4_image.clone()
|
|
image.data = image.data[:BLOCK_SIZE]
|
|
slot = DeviceSlot(2, 2)
|
|
write_device_table(image, [slot])
|
|
current = struct.unpack_from("<I", image.data, index_offset + 4)[0]
|
|
if current != 1:
|
|
raise FixtureError("LZ4 pcluster HEAD changed before relocation")
|
|
struct.pack_into("<I", image.data, index_offset + 4, slot.uniaddr)
|
|
return MultiFixture(image, [provider], [slot]), index_offset
|
|
|
|
|
|
def make_pcluster_crossing(
|
|
positive: MultiFixture, index_offset: int
|
|
) -> MultiFixture:
|
|
image = positive.primary.clone()
|
|
providers = [
|
|
bytearray(positive.providers[0][:BLOCK_SIZE]),
|
|
bytearray(positive.providers[0][BLOCK_SIZE:]),
|
|
]
|
|
slots = [DeviceSlot(1, 2), DeviceSlot(1, 3)]
|
|
write_device_table(image, slots)
|
|
current = struct.unpack_from("<I", image.data, index_offset + 4)[0]
|
|
if current != 2:
|
|
raise FixtureError("LZ4 crossing source HEAD changed")
|
|
return MultiFixture(image, providers, slots)
|
|
|
|
|
|
def image_description(image: ErofsImage, paths: list[str]) -> dict[str, Any]:
|
|
extra, slot_offset = struct.unpack_from("<HH", image.data, SUPER + 86)
|
|
description: dict[str, Any] = {
|
|
"provider_bytes": len(image.data),
|
|
"block_size": image.block_size,
|
|
"blocks": image.blocks,
|
|
"root_nid": image.root_nid,
|
|
"feature_compat": image.feature_compat,
|
|
"feature_incompat": image.feature_incompat,
|
|
"extra_devices": extra,
|
|
"devt_slotoff": slot_offset,
|
|
"checksum": image.u32(SUPER + 4),
|
|
"checksum_valid": image.checksum_valid(),
|
|
"slots": [slot.__dict__ for slot in decode_slots(image)] if extra else [],
|
|
"chunks": {},
|
|
}
|
|
for path in paths:
|
|
layout = chunk_layout(image, path)
|
|
description["chunks"][path] = {
|
|
"nid": layout.nid,
|
|
"inode_offset": layout.inode_offset,
|
|
"format_offset": layout.format_offset,
|
|
"format": layout.chunk_format,
|
|
"chunk_bits": layout.chunk_bits,
|
|
"chunk_size": layout.chunk_size,
|
|
"entry_size": layout.entry_size,
|
|
"index_base": layout.index_base,
|
|
"indexes": [
|
|
{
|
|
"offset": entry.offset,
|
|
"startblk_hi": entry.start_block_high,
|
|
"device_id": entry.device_id,
|
|
"startblk_lo": entry.start_block_low,
|
|
"startblk": entry.start_block,
|
|
}
|
|
for entry in layout.entries
|
|
],
|
|
}
|
|
return description
|
|
|
|
|
|
def compare_trees(source: Path, extracted: Path) -> None:
|
|
source_files = sorted(
|
|
path.relative_to(source) for path in source.rglob("*") if path.is_file()
|
|
)
|
|
extracted_files = sorted(
|
|
path.relative_to(extracted)
|
|
for path in extracted.rglob("*")
|
|
if path.is_file()
|
|
)
|
|
if source_files != extracted_files:
|
|
raise FixtureError(
|
|
f"extracted paths differ: {source_files} != {extracted_files}"
|
|
)
|
|
for relative in source_files:
|
|
if sha256_file(source / relative) != sha256_file(extracted / relative):
|
|
raise FixtureError(f"extracted checksum differs: {relative}")
|
|
|
|
|
|
def fsck_extract(
|
|
fsck: str,
|
|
primary: Path,
|
|
devices: list[Path],
|
|
source: Path,
|
|
scratch_parent: Path,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory(prefix="repo22-g6-fsck-", dir=scratch_parent) as tmp:
|
|
extracted = Path(tmp) / "extracted"
|
|
command = [fsck]
|
|
command.extend(f"--device={device}" for device in devices)
|
|
command.extend([f"--extract={extracted}", str(primary)])
|
|
run(command)
|
|
compare_trees(source, extracted)
|
|
|
|
|
|
def write_provider(path: Path, data: bytes | bytearray) -> None:
|
|
if len(data) == 0 or len(data) % BLOCK_SIZE != 0:
|
|
raise FixtureError(f"{path.name}: provider is not block aligned")
|
|
path.write_bytes(data)
|
|
|
|
|
|
def save_multifixture(
|
|
output: Path,
|
|
name: str,
|
|
fixture: MultiFixture,
|
|
paths: list[str],
|
|
manifest: dict[str, Any],
|
|
) -> tuple[Path, list[Path]]:
|
|
primary_path = output / "images" / f"{name}-primary.erofs"
|
|
finalize_image(fixture.primary, primary_path)
|
|
provider_paths = []
|
|
for index, provider in enumerate(fixture.providers, 1):
|
|
provider_path = output / "images" / f"{name}-slot{index}.blob"
|
|
write_provider(provider_path, provider)
|
|
provider_paths.append(provider_path)
|
|
manifest["fixtures"][name] = {
|
|
"primary": str(primary_path.relative_to(output)),
|
|
"providers": [str(path.relative_to(output)) for path in provider_paths],
|
|
"image": image_description(fixture.primary, paths),
|
|
}
|
|
return primary_path, provider_paths
|
|
|
|
|
|
def save_image_fixture(
|
|
output: Path,
|
|
name: str,
|
|
image: ErofsImage,
|
|
paths: list[str],
|
|
manifest: dict[str, Any],
|
|
) -> Path:
|
|
path = output / "images" / f"{name}.erofs"
|
|
finalize_image(image, path)
|
|
manifest["fixtures"][name] = {
|
|
"primary": str(path.relative_to(output)),
|
|
"providers": [],
|
|
"image": image_description(image, paths),
|
|
}
|
|
return path
|
|
|
|
|
|
def add_mutation(
|
|
manifest: dict[str, Any],
|
|
artifact: Path,
|
|
output: Path,
|
|
label: str,
|
|
offset: int,
|
|
fmt: str,
|
|
value: int,
|
|
) -> None:
|
|
manifest["mutations"].append(
|
|
{
|
|
"artifact": str(artifact.relative_to(output)),
|
|
"label": label,
|
|
"offset": offset,
|
|
"format": fmt,
|
|
"value": value,
|
|
}
|
|
)
|
|
|
|
|
|
def save_negative(
|
|
output: Path,
|
|
name: str,
|
|
image: ErofsImage,
|
|
manifest: dict[str, Any],
|
|
) -> Path:
|
|
path = output / "images" / f"{name}.erofs"
|
|
finalize_image(image, path)
|
|
manifest["negative_images"][name] = str(path.relative_to(output))
|
|
return path
|
|
|
|
|
|
def generate(args: argparse.Namespace) -> None:
|
|
output: Path = args.output.resolve()
|
|
if output.exists():
|
|
raise FixtureError(f"output already exists: {output}")
|
|
output.mkdir(parents=True)
|
|
(output / "images").mkdir()
|
|
chunk_source = output / "sources" / "chunk"
|
|
fragment_source = output / "sources" / "fragment"
|
|
pcluster_source = output / "sources" / "pcluster"
|
|
write_chunk_sources(chunk_source)
|
|
write_fragment_source(fragment_source)
|
|
write_pcluster_source(pcluster_source)
|
|
|
|
mkfs_version = tool_version(args.mkfs)
|
|
fsck_version = tool_version(args.fsck)
|
|
if "1.8.6" not in mkfs_version or "1.8.6" not in fsck_version:
|
|
raise FixtureError(
|
|
f"G6 requires erofs-utils 1.8.6, got {mkfs_version!r}, {fsck_version!r}"
|
|
)
|
|
|
|
base_primary_path = output / "images" / "mkfs-blob-primary.erofs"
|
|
base_blob_path = output / "images" / "mkfs-blob-slot1.blob"
|
|
base_blob_path.write_bytes(b"")
|
|
make_image(
|
|
args.mkfs,
|
|
base_primary_path,
|
|
chunk_source,
|
|
CHUNK_UUID,
|
|
["--chunksize=4096", f"--blobdev={base_blob_path}"],
|
|
)
|
|
baseline = ErofsImage.load(base_primary_path)
|
|
baseline.validate_superblock()
|
|
paths = [f"/{name}" for name in CHUNK_FILES]
|
|
assert_chunk_baseline(baseline, paths)
|
|
base_blob = base_blob_path.read_bytes()
|
|
if len(baseline.data) != BLOCK_SIZE or len(base_blob) % BLOCK_SIZE != 0:
|
|
raise FixtureError("mkfs blob baseline has unexpected provider sizes")
|
|
|
|
manifest: dict[str, Any] = {
|
|
"schema": 1,
|
|
"generator": "tests/g6_multidev_fixtures.py",
|
|
"erofs_utils": {"mkfs": mkfs_version, "fsck": fsck_version},
|
|
"fixtures": {},
|
|
"negative_images": {},
|
|
"mutations": [],
|
|
"artifacts": {},
|
|
"host_qualification": {
|
|
"flatdev": (
|
|
"kernel-only: erofs-utils 1.8.6 requires --device for "
|
|
"nonzero chunk device IDs"
|
|
),
|
|
"external_pcluster": (
|
|
"kernel-only after relocation: erofs-utils 1.8.6 does not "
|
|
"apply the device-ID-0 unified mapping used by the kernel"
|
|
),
|
|
},
|
|
}
|
|
manifest["fixtures"]["mkfs-blob"] = {
|
|
"primary": str(base_primary_path.relative_to(output)),
|
|
"providers": [str(base_blob_path.relative_to(output))],
|
|
"image": image_description(baseline, paths),
|
|
}
|
|
|
|
single = build_single_indexed(baseline, base_blob, paths)
|
|
single_path = save_image_fixture(
|
|
output, "single-indexed", single, paths, manifest
|
|
)
|
|
|
|
multi2 = build_multislot(baseline, base_blob, paths, 2)
|
|
multi2_primary, multi2_providers = save_multifixture(
|
|
output, "multi2", multi2, paths, manifest
|
|
)
|
|
flatdev_path = output / "images" / "multi2-flatdev.erofs"
|
|
write_provider(flatdev_path, build_flatdev(multi2))
|
|
manifest["fixtures"]["multi2-flatdev"] = {
|
|
"primary": str(flatdev_path.relative_to(output)),
|
|
"providers": [],
|
|
"image": image_description(ErofsImage.load(flatdev_path), paths),
|
|
}
|
|
|
|
multi3 = build_multislot(baseline, base_blob, paths, 3)
|
|
multi3_primary, multi3_providers = save_multifixture(
|
|
output, "multi3", multi3, paths, manifest
|
|
)
|
|
|
|
slot_zero = clone_multifixture(multi2)
|
|
zero_slots = [DeviceSlot(slot_zero.slots[0].blocks, 0), slot_zero.slots[1]]
|
|
rewrite_slots(slot_zero, zero_slots)
|
|
slot_zero_primary, slot_zero_providers = save_multifixture(
|
|
output, "multi2-uniaddr0", slot_zero, paths, manifest
|
|
)
|
|
|
|
table_zero = clone_multifixture(multi2)
|
|
table_bytes = bytes(
|
|
table_zero.primary.data[
|
|
BLOCK_SIZE : BLOCK_SIZE + 2 * DEVICE_SLOT_SIZE
|
|
]
|
|
)
|
|
table_zero.primary.data[: 2 * DEVICE_SLOT_SIZE] = table_bytes
|
|
table_zero.primary.put_u16(SUPER + 88, 0)
|
|
table_zero_primary, table_zero_providers = save_multifixture(
|
|
output, "multi2-devt0", table_zero, paths, manifest
|
|
)
|
|
|
|
unified = clone_multifixture(multi2)
|
|
patch_unified_entry(unified, "/unified-address.bin")
|
|
unified_primary, unified_providers = save_multifixture(
|
|
output, "multi2-unified", unified, paths, manifest
|
|
)
|
|
unified_flatdev_path = output / "images" / "multi2-unified-flatdev.erofs"
|
|
write_provider(unified_flatdev_path, build_flatdev(unified))
|
|
manifest["fixtures"]["multi2-unified-flatdev"] = {
|
|
"primary": str(unified_flatdev_path.relative_to(output)),
|
|
"providers": [],
|
|
"image": image_description(
|
|
ErofsImage.load(unified_flatdev_path), paths
|
|
),
|
|
}
|
|
|
|
unified48 = clone_multifixture(multi2)
|
|
enable_48bit(unified48.primary)
|
|
set_chunk_48bit(unified48.primary, paths)
|
|
high_start = (1 << 32) + 2
|
|
high_slots = []
|
|
cursor = high_start
|
|
for slot in unified48.slots:
|
|
high_slots.append(DeviceSlot(slot.blocks, cursor))
|
|
cursor += slot.blocks
|
|
rewrite_slots(unified48, high_slots)
|
|
patch_unified_entry(unified48, "/unified-address.bin")
|
|
unified48_primary, unified48_providers = save_multifixture(
|
|
output, "multi2-unified48", unified48, paths, manifest
|
|
)
|
|
|
|
unified48_oob = clone_multifixture(unified48)
|
|
unified48_oob_entry = first_entry(
|
|
unified48_oob.primary, "/unified-address.bin"
|
|
)
|
|
patch_chunk_entry(
|
|
unified48_oob.primary,
|
|
unified48_oob_entry,
|
|
expected=(
|
|
unified48_oob_entry.start_block_high,
|
|
0,
|
|
unified48_oob_entry.start_block_low,
|
|
),
|
|
replacement=(0xFFFF, 0, 0xFFFFFFFE),
|
|
)
|
|
unified48_oob_primary, unified48_oob_providers = save_multifixture(
|
|
output, "bad-unified48-out-of-range", unified48_oob, paths, manifest
|
|
)
|
|
add_mutation(
|
|
manifest,
|
|
unified48_oob_primary,
|
|
output,
|
|
"device-ID-0 chunk address is the largest non-NULL 48-bit block",
|
|
unified48_oob_entry.offset,
|
|
"<HHI",
|
|
[0xFFFF, 0, 0xFFFFFFFE],
|
|
)
|
|
|
|
mapped_id = multi2.primary.clone()
|
|
mapped_entry = first_entry(mapped_id, "/indexed.bin", 1)
|
|
patch_chunk_entry(
|
|
mapped_id,
|
|
mapped_entry,
|
|
expected=(0, mapped_entry.device_id, mapped_entry.start_block_low),
|
|
replacement=(0, 3, mapped_entry.start_block_low),
|
|
)
|
|
mapped_id_path = save_negative(
|
|
output, "bad-mapped-device3", mapped_id, manifest
|
|
)
|
|
add_mutation(
|
|
manifest,
|
|
mapped_id_path,
|
|
output,
|
|
"mapped device ID remains undeclared ID 3",
|
|
mapped_entry.offset + 2,
|
|
"<H",
|
|
3,
|
|
)
|
|
|
|
gap = clone_multifixture(multi2)
|
|
gap_slots = [
|
|
gap.slots[0],
|
|
DeviceSlot(
|
|
gap.slots[1].blocks,
|
|
gap.slots[0].uniaddr + gap.slots[0].blocks + 2,
|
|
),
|
|
]
|
|
rewrite_slots(gap, gap_slots)
|
|
gap_entry = first_entry(gap.primary, "/unified-address.bin")
|
|
gap_block = gap.slots[0].uniaddr + gap.slots[0].blocks
|
|
patch_chunk_entry(
|
|
gap.primary,
|
|
gap_entry,
|
|
expected=(0, gap_entry.device_id, gap_entry.start_block_low),
|
|
replacement=(0, 0, gap_block),
|
|
)
|
|
gap_primary, gap_providers = save_multifixture(
|
|
output, "bad-unified-gap", gap, paths, manifest
|
|
)
|
|
gap_flatdev_path = output / "images" / "bad-unified-gap-flatdev.erofs"
|
|
write_provider(gap_flatdev_path, build_flatdev(gap))
|
|
add_mutation(
|
|
manifest,
|
|
gap_primary,
|
|
output,
|
|
"device-ID-0 chunk points at a declared-address gap",
|
|
gap_entry.offset + 2,
|
|
"<HI",
|
|
[0, gap_block],
|
|
)
|
|
|
|
cross_explicit = clone_multifixture(multi2)
|
|
cross_entry = first_entry(cross_explicit.primary, "/indexed.bin")
|
|
cross_local = cross_explicit.slots[0].blocks - 1
|
|
patch_chunk_entry(
|
|
cross_explicit.primary,
|
|
cross_entry,
|
|
expected=(0, cross_entry.device_id, cross_entry.start_block_low),
|
|
replacement=(0, 1, cross_local),
|
|
)
|
|
cross_explicit_primary, cross_explicit_providers = save_multifixture(
|
|
output, "bad-cross-slot-explicit", cross_explicit, paths, manifest
|
|
)
|
|
|
|
cross_unified = clone_multifixture(multi2)
|
|
cross_unified_entry = first_entry(cross_unified.primary, "/indexed.bin")
|
|
cross_global = (
|
|
cross_unified.slots[0].uniaddr + cross_unified.slots[0].blocks - 1
|
|
)
|
|
patch_chunk_entry(
|
|
cross_unified.primary,
|
|
cross_unified_entry,
|
|
expected=(
|
|
0,
|
|
cross_unified_entry.device_id,
|
|
cross_unified_entry.start_block_low,
|
|
),
|
|
replacement=(0, 0, cross_global),
|
|
)
|
|
cross_unified_primary, cross_unified_providers = save_multifixture(
|
|
output, "bad-cross-slot-unified", cross_unified, paths, manifest
|
|
)
|
|
cross_unified_flatdev_path = (
|
|
output / "images" / "bad-cross-slot-unified-flatdev.erofs"
|
|
)
|
|
write_provider(cross_unified_flatdev_path, build_flatdev(cross_unified))
|
|
|
|
invalid_builders: list[
|
|
tuple[str, Callable[[ErofsImage], tuple[int, str, int, str]]]
|
|
] = []
|
|
|
|
def table_oob(image: ErofsImage) -> tuple[int, str, int, str]:
|
|
value = image.blocks * BLOCK_SIZE // DEVICE_SLOT_SIZE
|
|
image.put_u16(SUPER + 88, value)
|
|
return SUPER + 88, "<H", value, "device table starts at image end"
|
|
|
|
invalid_builders.append(("bad-table-oob", table_oob))
|
|
|
|
def zero_blocks(image: ErofsImage) -> tuple[int, str, int, str]:
|
|
patch_table_field(image, 1, 0, "<I", 0)
|
|
return BLOCK_SIZE + 64, "<I", 0, "slot 1 has zero blocks"
|
|
|
|
invalid_builders.append(("bad-slot-zero-blocks", zero_blocks))
|
|
|
|
def inside_primary(image: ErofsImage) -> tuple[int, str, int, str]:
|
|
patch_table_field(image, 1, 4, "<I", 1)
|
|
return BLOCK_SIZE + 68, "<I", 1, "slot 1 begins in primary range"
|
|
|
|
invalid_builders.append(("bad-slot-inside-primary", inside_primary))
|
|
|
|
def overlap(image: ErofsImage) -> tuple[int, str, int, str]:
|
|
value = multi2.slots[0].uniaddr
|
|
patch_table_field(image, 2, 4, "<I", value)
|
|
return (
|
|
BLOCK_SIZE + DEVICE_SLOT_SIZE + 68,
|
|
"<I",
|
|
value,
|
|
"slot 2 overlaps slot 1",
|
|
)
|
|
|
|
invalid_builders.append(("bad-slot-overlap", overlap))
|
|
|
|
def range32(image: ErofsImage) -> tuple[int, str, int, str]:
|
|
patch_table_field(image, 1, 0, "<I", 2)
|
|
patch_table_field(image, 1, 4, "<I", 0xFFFFFFFF)
|
|
return (
|
|
BLOCK_SIZE + 68,
|
|
"<I",
|
|
0xFFFFFFFF,
|
|
"non-48-bit range ends above 2^32",
|
|
)
|
|
|
|
invalid_builders.append(("bad-slot-32bit-limit", range32))
|
|
|
|
def range48(image: ErofsImage) -> tuple[int, str, int, str]:
|
|
enable_48bit(image)
|
|
patch_table_field(image, 1, 0, "<I", 2)
|
|
patch_table_field(image, 1, 4, "<I", 0xFFFFFFFF)
|
|
patch_table_field(image, 1, 10, "<H", 0xFFFF)
|
|
return (
|
|
BLOCK_SIZE + 74,
|
|
"<H",
|
|
0xFFFF,
|
|
"48-bit range ends above 2^48",
|
|
)
|
|
|
|
invalid_builders.append(("bad-slot-48bit-limit", range48))
|
|
|
|
for name, builder in invalid_builders:
|
|
invalid = multi2.primary.clone()
|
|
offset, fmt, value, label = builder(invalid)
|
|
path = save_negative(output, name, invalid, manifest)
|
|
add_mutation(manifest, path, output, label, offset, fmt, value)
|
|
|
|
fragment_base_path = output / "images" / "fragment.erofs"
|
|
make_image(
|
|
args.mkfs,
|
|
fragment_base_path,
|
|
fragment_source,
|
|
FRAGMENT_UUID,
|
|
["-zlz4", "-C65536", "-E", "all-fragments"],
|
|
)
|
|
fragment = ErofsImage.load(fragment_base_path)
|
|
_, fragment_entry = fragment.resolve_root_entry("/fragment.dat")
|
|
fragment_inode = fragment.inode(fragment_entry.nid)
|
|
packed_nid = fragment.u64(SUPER + 96)
|
|
if packed_nid == 0 or fragment_inode.layout not in (1, 3):
|
|
raise FixtureError("mkfs fragment fixture lacks packed fragment inode")
|
|
manifest["fixtures"]["fragment"] = {
|
|
"primary": str(fragment_base_path.relative_to(output)),
|
|
"providers": [],
|
|
"packed_nid": packed_nid,
|
|
"fragment_nid": fragment_inode.nid,
|
|
"fragment_layout": fragment_inode.layout,
|
|
"image": image_description(fragment, []),
|
|
}
|
|
|
|
lz4_base_path = output / "images" / "lz4-two-block-base.erofs"
|
|
make_image(
|
|
args.mkfs,
|
|
lz4_base_path,
|
|
pcluster_source,
|
|
PCLUSTER_UUID,
|
|
["-zlz4", "-C65536", "-E", "legacy-compress"],
|
|
)
|
|
lz4 = ErofsImage.load(lz4_base_path)
|
|
pcluster, pcluster_index_offset = make_pcluster_external(lz4)
|
|
pcluster_primary, pcluster_providers = save_multifixture(
|
|
output, "lz4-external-pcluster", pcluster, [], manifest
|
|
)
|
|
manifest["fixtures"]["lz4-external-pcluster"]["head_index_offset"] = (
|
|
pcluster_index_offset
|
|
)
|
|
manifest["fixtures"]["lz4-external-pcluster"]["compressed_bytes"] = 8192
|
|
add_mutation(
|
|
manifest,
|
|
pcluster_primary,
|
|
output,
|
|
"LZ4 HEAD pblk selects external unified range",
|
|
pcluster_index_offset + 4,
|
|
"<I",
|
|
2,
|
|
)
|
|
pcluster_cross = make_pcluster_crossing(pcluster, pcluster_index_offset)
|
|
pcluster_cross_primary, pcluster_cross_providers = save_multifixture(
|
|
output, "bad-lz4-cross-slot", pcluster_cross, [], manifest
|
|
)
|
|
pcluster_cross_flatdev = output / "images" / "bad-lz4-cross-slot-flatdev.erofs"
|
|
write_provider(pcluster_cross_flatdev, build_flatdev(pcluster_cross))
|
|
|
|
short_slot2 = output / "images" / "multi2-slot2-short.blob"
|
|
if len(multi2.providers[1]) <= BLOCK_SIZE:
|
|
raise FixtureError("slot 2 is too short to create truncation fixture")
|
|
write_provider(short_slot2, multi2.providers[1][:-BLOCK_SIZE])
|
|
short_slot3 = output / "images" / "multi3-slot3-short.blob"
|
|
if len(multi3.providers[2]) <= BLOCK_SIZE:
|
|
raise FixtureError("slot 3 is too short to create truncation fixture")
|
|
write_provider(short_slot3, multi3.providers[2][:-BLOCK_SIZE])
|
|
|
|
fsck_extract(
|
|
args.fsck,
|
|
base_primary_path,
|
|
[base_blob_path],
|
|
chunk_source,
|
|
output.parent,
|
|
)
|
|
fsck_extract(args.fsck, single_path, [], chunk_source, output.parent)
|
|
fsck_extract(
|
|
args.fsck,
|
|
multi2_primary,
|
|
multi2_providers,
|
|
chunk_source,
|
|
output.parent,
|
|
)
|
|
fsck_extract(
|
|
args.fsck,
|
|
multi3_primary,
|
|
multi3_providers,
|
|
chunk_source,
|
|
output.parent,
|
|
)
|
|
fsck_extract(
|
|
args.fsck,
|
|
slot_zero_primary,
|
|
slot_zero_providers,
|
|
chunk_source,
|
|
output.parent,
|
|
)
|
|
fsck_extract(
|
|
args.fsck,
|
|
table_zero_primary,
|
|
table_zero_providers,
|
|
chunk_source,
|
|
output.parent,
|
|
)
|
|
fsck_extract(
|
|
args.fsck,
|
|
fragment_base_path,
|
|
[],
|
|
fragment_source,
|
|
output.parent,
|
|
)
|
|
fsck_extract(
|
|
args.fsck, lz4_base_path, [], pcluster_source, output.parent
|
|
)
|
|
|
|
for path in sorted(output.rglob("*")):
|
|
if path.is_file() and path.name not in ("manifest.json", "SHA256SUMS"):
|
|
relative = str(path.relative_to(output))
|
|
manifest["artifacts"][relative] = {
|
|
"bytes": path.stat().st_size,
|
|
"sha256": sha256_file(path),
|
|
}
|
|
manifest_path = output / "manifest.json"
|
|
manifest_path.write_text(
|
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
|
)
|
|
with (output / "SHA256SUMS").open("w", encoding="ascii") as sums:
|
|
for relative, record in sorted(manifest["artifacts"].items()):
|
|
sums.write(f"{record['sha256']} {relative}\n")
|
|
sums.write(f"{sha256_file(manifest_path)} manifest.json\n")
|
|
verify_output(output)
|
|
print(
|
|
f"generated={output} artifacts={len(manifest['artifacts'])} "
|
|
f"fixtures={len(manifest['fixtures'])} negatives={len(manifest['negative_images'])}"
|
|
)
|
|
|
|
|
|
def unpack_assertion(path: Path, offset: int, fmt: str) -> Any:
|
|
size = struct.calcsize(fmt)
|
|
with path.open("rb") as stream:
|
|
stream.seek(offset)
|
|
data = stream.read(size)
|
|
if len(data) != size:
|
|
raise FixtureError(f"{path}: field at {offset} is truncated")
|
|
values = struct.unpack(fmt, data)
|
|
return values[0] if len(values) == 1 else list(values)
|
|
|
|
|
|
def verify_image_description(path: Path, expected: dict[str, Any]) -> None:
|
|
image = ErofsImage.load(path)
|
|
paths = list(expected["chunks"])
|
|
actual = image_description(image, paths)
|
|
if actual != expected:
|
|
raise FixtureError(f"{path}: parsed image fields differ from manifest")
|
|
|
|
|
|
def verify_output(output: Path) -> None:
|
|
manifest_path = output / "manifest.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="ascii"))
|
|
if manifest.get("schema") != 1:
|
|
raise FixtureError("unsupported G6 manifest schema")
|
|
for relative, expected in manifest["artifacts"].items():
|
|
path = output / relative
|
|
if not path.is_file():
|
|
raise FixtureError(f"missing artifact: {relative}")
|
|
if path.stat().st_size != expected["bytes"]:
|
|
raise FixtureError(f"size differs: {relative}")
|
|
if sha256_file(path) != expected["sha256"]:
|
|
raise FixtureError(f"SHA256 differs: {relative}")
|
|
for fixture in manifest["fixtures"].values():
|
|
image = fixture.get("image")
|
|
if image is not None:
|
|
verify_image_description(output / fixture["primary"], image)
|
|
for mutation in manifest["mutations"]:
|
|
actual = unpack_assertion(
|
|
output / mutation["artifact"],
|
|
mutation["offset"],
|
|
mutation["format"],
|
|
)
|
|
if actual != mutation["value"]:
|
|
raise FixtureError(
|
|
f"{mutation['label']}: expected {mutation['value']}, got {actual}"
|
|
)
|
|
run(["sha256sum", "-c", "SHA256SUMS"], stdout=True, cwd=output)
|
|
print(
|
|
f"verified={output} artifacts={len(manifest['artifacts'])} "
|
|
f"field_assertions={len(manifest['mutations'])}"
|
|
)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
generate_parser = subparsers.add_parser("generate")
|
|
generate_parser.add_argument("--output", required=True, type=Path)
|
|
generate_parser.add_argument("--mkfs", default=shutil.which("mkfs.erofs"))
|
|
generate_parser.add_argument("--fsck", default=shutil.which("fsck.erofs"))
|
|
generate_parser.set_defaults(function=generate)
|
|
|
|
verify_parser = subparsers.add_parser("verify")
|
|
verify_parser.add_argument("output", type=Path)
|
|
verify_parser.set_defaults(function=lambda args: verify_output(args.output.resolve()))
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
if args.command == "generate" and (args.mkfs is None or args.fsck is None):
|
|
raise FixtureError("mkfs.erofs and fsck.erofs are required")
|
|
args.function(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
|
|
main()
|