609 lines
22 KiB
Python
609 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""Build and self-check fixtures for TC154-TC156."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
|
|
|
|
SUPER = 1024
|
|
EROFS_MAGIC = 0xE0F5E1E2
|
|
FEATURE_COMPAT_SB_CHKSUM = 0x1
|
|
EROFS_INODE_FLAT_PLAIN = 0
|
|
EROFS_INODE_FLAT_INLINE = 2
|
|
EROFS_INODE_COMPRESSED_FULL = 1
|
|
Z_EROFS_ADVISE_EXTENTS = 0x1
|
|
UINT64_MAX = (1 << 64) - 1
|
|
INT64_MAX = (1 << 63) - 1
|
|
FNV1_32_INIT = 33554467
|
|
FNV_32_PRIME = 0x01000193
|
|
|
|
|
|
class FixtureError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class Inode:
|
|
def __init__(
|
|
self,
|
|
nid: int,
|
|
offset: int,
|
|
inode_format: int,
|
|
inode_size: int,
|
|
xattr_size: int,
|
|
layout: int,
|
|
size: int,
|
|
start_block: int,
|
|
) -> None:
|
|
self.nid = nid
|
|
self.offset = offset
|
|
self.inode_format = inode_format
|
|
self.inode_size = inode_size
|
|
self.xattr_size = xattr_size
|
|
self.layout = layout
|
|
self.size = size
|
|
self.start_block = start_block
|
|
|
|
|
|
class ErofsImage:
|
|
def __init__(self, data: bytes | bytearray) -> None:
|
|
self.data = bytearray(data)
|
|
self.validate_superblock()
|
|
|
|
@classmethod
|
|
def load(cls, path: Path) -> "ErofsImage":
|
|
return cls(path.read_bytes())
|
|
|
|
def clone(self) -> "ErofsImage":
|
|
return ErofsImage(self.data)
|
|
|
|
def u16(self, offset: int) -> int:
|
|
return struct.unpack_from("<H", self.data, offset)[0]
|
|
|
|
def u32(self, offset: int) -> int:
|
|
return struct.unpack_from("<I", self.data, offset)[0]
|
|
|
|
def u64(self, offset: int) -> int:
|
|
return struct.unpack_from("<Q", self.data, offset)[0]
|
|
|
|
def put_u16(self, offset: int, value: int) -> None:
|
|
struct.pack_into("<H", self.data, offset, value)
|
|
|
|
def put_u32(self, offset: int, value: int) -> None:
|
|
struct.pack_into("<I", self.data, offset, value)
|
|
|
|
def put_u64(self, offset: int, value: int) -> None:
|
|
struct.pack_into("<Q", self.data, offset, value)
|
|
|
|
@property
|
|
def block_bits(self) -> int:
|
|
return self.data[SUPER + 12]
|
|
|
|
@property
|
|
def block_size(self) -> int:
|
|
if not 9 <= self.block_bits <= 16:
|
|
raise FixtureError(f"invalid block bits: {self.block_bits}")
|
|
return 1 << self.block_bits
|
|
|
|
@property
|
|
def super_size(self) -> int:
|
|
return 128 + self.data[SUPER + 13] * 16
|
|
|
|
@property
|
|
def checksum_end(self) -> int:
|
|
length = self.block_size
|
|
if length > SUPER:
|
|
length -= SUPER
|
|
return SUPER + length
|
|
|
|
@property
|
|
def feature_compat(self) -> int:
|
|
return self.u32(SUPER + 8)
|
|
|
|
@property
|
|
def root_nid(self) -> int:
|
|
feature_incompat = self.u32(SUPER + 80)
|
|
root_nid_8b = self.u64(SUPER + 112)
|
|
if feature_incompat & 0x80 and root_nid_8b != 0:
|
|
return root_nid_8b
|
|
return self.u16(SUPER + 14)
|
|
|
|
def calculated_checksum(self) -> int:
|
|
window = bytearray(self.data[SUPER : self.checksum_end])
|
|
struct.pack_into("<I", window, 4, 0)
|
|
checksum = 0xFFFFFFFF
|
|
polynomial = 0x82F63B78
|
|
for byte in window:
|
|
checksum ^= byte
|
|
for _ in range(8):
|
|
checksum = (checksum >> 1) ^ (
|
|
polynomial if checksum & 1 else 0
|
|
)
|
|
return checksum & 0xFFFFFFFF
|
|
|
|
def checksum_valid(self) -> bool:
|
|
if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM:
|
|
return True
|
|
return self.u32(SUPER + 4) == self.calculated_checksum()
|
|
|
|
def update_checksum(self) -> None:
|
|
if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM:
|
|
raise FixtureError("base image lacks superblock checksum support")
|
|
self.put_u32(SUPER + 4, 0)
|
|
self.put_u32(SUPER + 4, self.calculated_checksum())
|
|
if not self.checksum_valid():
|
|
raise FixtureError("updated superblock checksum does not verify")
|
|
|
|
def validate_superblock(self) -> None:
|
|
if len(self.data) < SUPER + 144:
|
|
raise FixtureError("image is too short for an EROFS superblock")
|
|
if self.u32(SUPER) != EROFS_MAGIC:
|
|
raise FixtureError(f"bad EROFS magic: {self.u32(SUPER):#x}")
|
|
if SUPER + self.super_size > len(self.data):
|
|
raise FixtureError("declared superblock is truncated")
|
|
if not self.checksum_valid():
|
|
raise FixtureError("superblock checksum is invalid")
|
|
|
|
def inode(self, nid: int) -> Inode:
|
|
metadata = self.u32(SUPER + 40) << self.block_bits
|
|
offset = metadata + (nid << 5)
|
|
if offset > len(self.data) - 32:
|
|
raise FixtureError(f"nid {nid} lies outside the image")
|
|
inode_format = self.u16(offset)
|
|
inode_size = 64 if inode_format & 1 else 32
|
|
if offset > len(self.data) - inode_size:
|
|
raise FixtureError(f"nid {nid} has a truncated inode")
|
|
xattr_count = self.u16(offset + 2)
|
|
xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1)
|
|
size = (
|
|
self.u64(offset + 8)
|
|
if inode_size == 64
|
|
else self.u32(offset + 8)
|
|
)
|
|
return Inode(
|
|
nid=nid,
|
|
offset=offset,
|
|
inode_format=inode_format,
|
|
inode_size=inode_size,
|
|
xattr_size=xattr_size,
|
|
layout=(inode_format >> 1) & 7,
|
|
size=size,
|
|
start_block=self.u32(offset + 16),
|
|
)
|
|
|
|
def directory_data(self, inode: Inode) -> bytes:
|
|
if inode.size == 0:
|
|
raise FixtureError("empty directory cannot contain a target")
|
|
if inode.layout == EROFS_INODE_FLAT_PLAIN:
|
|
offset = inode.start_block << self.block_bits
|
|
end = offset + inode.size
|
|
if end > len(self.data):
|
|
raise FixtureError("plain directory data is truncated")
|
|
return bytes(self.data[offset:end])
|
|
if inode.layout != EROFS_INODE_FLAT_INLINE:
|
|
raise FixtureError(f"unsupported directory layout {inode.layout}")
|
|
tail_start = ((inode.size + self.block_size - 1) // self.block_size - 1)
|
|
tail_start *= self.block_size
|
|
full_offset = inode.start_block << self.block_bits
|
|
inline_offset = inode.offset + inode.inode_size + inode.xattr_size
|
|
full = self.data[full_offset : full_offset + tail_start]
|
|
tail = self.data[inline_offset : inline_offset + inode.size - tail_start]
|
|
if len(full) + len(tail) != inode.size:
|
|
raise FixtureError("inline directory data is truncated")
|
|
return bytes(full + tail)
|
|
|
|
def directory_entries(self, inode: Inode) -> list[tuple[bytes, int]]:
|
|
data = self.directory_data(inode)
|
|
entries: list[tuple[bytes, int]] = []
|
|
for block_start in range(0, len(data), self.block_size):
|
|
block = data[block_start : block_start + self.block_size]
|
|
if len(block) < 12:
|
|
raise FixtureError("short directory block")
|
|
first_nameoff = struct.unpack_from("<H", block, 8)[0]
|
|
if (
|
|
first_nameoff < 12
|
|
or first_nameoff % 12 != 0
|
|
or first_nameoff >= len(block)
|
|
):
|
|
raise FixtureError("invalid first directory name offset")
|
|
count = first_nameoff // 12
|
|
previous = 0
|
|
for index in range(count):
|
|
entry_offset = index * 12
|
|
nid = struct.unpack_from("<Q", block, entry_offset)[0]
|
|
nameoff = struct.unpack_from("<H", block, entry_offset + 8)[0]
|
|
endoff = (
|
|
struct.unpack_from("<H", block, entry_offset + 20)[0]
|
|
if index + 1 < count
|
|
else len(block)
|
|
)
|
|
if (
|
|
nameoff < first_nameoff
|
|
or nameoff <= previous
|
|
or endoff <= nameoff
|
|
or endoff > len(block)
|
|
):
|
|
raise FixtureError("invalid directory name range")
|
|
name = block[nameoff:endoff].split(b"\0", 1)[0]
|
|
if not name:
|
|
raise FixtureError("empty directory name")
|
|
entries.append((name, nid))
|
|
previous = nameoff
|
|
return entries
|
|
|
|
def resolve_root_entry(self, name: str) -> Inode:
|
|
encoded = name.encode("ascii")
|
|
root = self.inode(self.root_nid)
|
|
for entry_name, nid in self.directory_entries(root):
|
|
if entry_name == encoded:
|
|
return self.inode(nid)
|
|
raise FixtureError(f"root entry not found: {name}")
|
|
|
|
def inode_bytes(self, inode: Inode) -> bytes:
|
|
return bytes(self.data[inode.offset : inode.offset + inode.inode_size])
|
|
|
|
def generation(self, inode: Inode) -> int:
|
|
seed = fnv1_32(
|
|
bytes(self.data[SUPER : SUPER + self.super_size]), FNV1_32_INIT
|
|
)
|
|
generation = fnv1_32(struct.pack("<Q", inode.nid), seed)
|
|
generation = fnv1_32(self.inode_bytes(inode), generation)
|
|
return generation or 1
|
|
|
|
def save(self, path: Path) -> None:
|
|
self.validate_superblock()
|
|
path.write_bytes(self.data)
|
|
|
|
|
|
def fnv1_32(data: bytes, initial: int) -> int:
|
|
value = initial
|
|
for byte in data:
|
|
value = (value * FNV_32_PRIME) & 0xFFFFFFFF
|
|
value ^= byte
|
|
return value
|
|
|
|
|
|
def align(value: int, alignment: int) -> int:
|
|
return (value + alignment - 1) & -alignment
|
|
|
|
|
|
def run_mkfs(source: Path, image: Path, uuid: str, *extra: str) -> None:
|
|
command = [
|
|
"mkfs.erofs",
|
|
"-d0",
|
|
"-x-1",
|
|
"-T0",
|
|
"--all-time",
|
|
"--all-root",
|
|
"--workers=1",
|
|
"--sort=path",
|
|
"-U",
|
|
uuid,
|
|
*extra,
|
|
str(image),
|
|
str(source),
|
|
]
|
|
subprocess.run(command, check=True)
|
|
|
|
|
|
def normalize_times(root: Path) -> None:
|
|
for path in sorted(root.rglob("*"), reverse=True):
|
|
os.utime(path, (0, 0), follow_symlinks=False)
|
|
os.utime(root, (0, 0), follow_symlinks=False)
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def make_fixtures(output: Path) -> None:
|
|
if shutil.which("mkfs.erofs") is None:
|
|
raise FixtureError("mkfs.erofs is required")
|
|
if output.exists() and any(output.iterdir()):
|
|
raise FixtureError(f"output directory is not empty: {output}")
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
source = output / "source"
|
|
nfs_source = source / "nfs"
|
|
extent_source = source / "extent"
|
|
extended_source = source / "extended"
|
|
nfs_source.mkdir(parents=True)
|
|
extent_source.mkdir(parents=True)
|
|
extended_source.mkdir(parents=True)
|
|
|
|
for index in range(192):
|
|
(nfs_source / f"filler-{index:03d}.txt").write_text(
|
|
f"filler {index:03d}\n", encoding="ascii"
|
|
)
|
|
(nfs_source / "zz-identity.txt").write_text(
|
|
"stable inode identity\n", encoding="ascii"
|
|
)
|
|
(extent_source / "extent.bin").write_bytes(b"E" * (1024 * 1024))
|
|
(extended_source / "extended-time.txt").write_text(
|
|
"extended timestamp\n", encoding="ascii"
|
|
)
|
|
normalize_times(source)
|
|
|
|
nfs_base_path = output / ".nfs-base.erofs"
|
|
extent_base_path = output / ".extent-base.erofs"
|
|
extended_base_path = output / ".extended-base.erofs"
|
|
run_mkfs(
|
|
nfs_source,
|
|
nfs_base_path,
|
|
"66666666-7777-4888-9999-aaaaaaaaaa54",
|
|
"-E",
|
|
"force-inode-compact",
|
|
)
|
|
run_mkfs(
|
|
extent_source,
|
|
extent_base_path,
|
|
"66666666-7777-4888-9999-aaaaaaaaaa55",
|
|
"-E",
|
|
"legacy-compress,force-inode-extended",
|
|
"-z",
|
|
"lz4",
|
|
"-C4096",
|
|
)
|
|
run_mkfs(
|
|
extended_source,
|
|
extended_base_path,
|
|
"66666666-7777-4888-9999-aaaaaaaaaa56",
|
|
"-E",
|
|
"force-inode-extended",
|
|
)
|
|
|
|
evidence: list[str] = []
|
|
manifest: dict[str, object] = {}
|
|
|
|
nfs_a = ErofsImage.load(nfs_base_path)
|
|
identity_a = nfs_a.resolve_root_entry("zz-identity.txt")
|
|
if identity_a.inode_size != 32:
|
|
raise FixtureError("NFS identity target is not a compact inode")
|
|
if identity_a.offset < nfs_a.checksum_end:
|
|
raise FixtureError("NFS identity inode overlaps the checksum window")
|
|
nfs_a_path = output / "nfs-inode-a.erofs"
|
|
nfs_a.save(nfs_a_path)
|
|
|
|
nfs_b = nfs_a.clone()
|
|
identity_b = nfs_b.resolve_root_entry("zz-identity.txt")
|
|
old_mtime = nfs_b.u32(identity_b.offset + 12)
|
|
nfs_b.put_u32(identity_b.offset + 12, old_mtime + 1)
|
|
nfs_b_path = output / "nfs-inode-b.erofs"
|
|
nfs_b.save(nfs_b_path)
|
|
nfs_differences = [
|
|
offset
|
|
for offset, (left, right) in enumerate(zip(nfs_a.data, nfs_b.data))
|
|
if left != right
|
|
]
|
|
mtime_offsets = range(identity_b.offset + 12, identity_b.offset + 16)
|
|
if not nfs_differences or any(
|
|
offset not in mtime_offsets for offset in nfs_differences
|
|
):
|
|
raise FixtureError("NFS replacement changed data outside i_mtime")
|
|
if nfs_a.data[: nfs_a.checksum_end] != nfs_b.data[: nfs_b.checksum_end]:
|
|
raise FixtureError("NFS replacement changed the superblock block")
|
|
if nfs_a.data[SUPER : SUPER + nfs_a.super_size] != nfs_b.data[
|
|
SUPER : SUPER + nfs_b.super_size
|
|
]:
|
|
raise FixtureError("NFS replacement changed the declared superblock")
|
|
if nfs_a.inode_bytes(identity_a) == nfs_b.inode_bytes(identity_b):
|
|
raise FixtureError("NFS replacement did not change inode metadata")
|
|
generation_a = nfs_a.generation(identity_a)
|
|
generation_b = nfs_b.generation(identity_b)
|
|
if generation_a == generation_b:
|
|
raise FixtureError("per-inode generation did not change")
|
|
|
|
nfs_corrupt = nfs_a.clone()
|
|
corrupt_inode = nfs_corrupt.resolve_root_entry("zz-identity.txt")
|
|
nfs_corrupt.put_u16(
|
|
corrupt_inode.offset, nfs_corrupt.u16(corrupt_inode.offset) | 0x8000
|
|
)
|
|
nfs_corrupt_path = output / "nfs-inode-corrupt.erofs"
|
|
nfs_corrupt.save(nfs_corrupt_path)
|
|
if nfs_a.data[: nfs_a.checksum_end] != nfs_corrupt.data[: nfs_a.checksum_end]:
|
|
raise FixtureError("corrupt inode fixture changed the superblock block")
|
|
|
|
evidence.append(
|
|
"nfs "
|
|
f"nid={identity_a.nid} inode_offset={identity_a.offset} "
|
|
f"checksum_end={nfs_a.checksum_end} mtime={old_mtime}->{old_mtime + 1} "
|
|
f"generation={generation_a}->{generation_b} superblock_block=identical"
|
|
)
|
|
manifest["nfs"] = {
|
|
"path": "/zz-identity.txt",
|
|
"nid": identity_a.nid,
|
|
"inode_offset": identity_a.offset,
|
|
"generation_a": generation_a,
|
|
"generation_b": generation_b,
|
|
"super_size": nfs_a.super_size,
|
|
"checksum_end": nfs_a.checksum_end,
|
|
}
|
|
|
|
extent = ErofsImage.load(extent_base_path)
|
|
extent_inode = extent.resolve_root_entry("extent.bin")
|
|
if (
|
|
extent_inode.inode_size != 64
|
|
or extent_inode.layout != EROFS_INODE_COMPRESSED_FULL
|
|
):
|
|
raise FixtureError("extent target is not an extended full-index inode")
|
|
header = align(
|
|
extent_inode.offset + extent_inode.inode_size + extent_inode.xattr_size,
|
|
8,
|
|
)
|
|
physical_base_offset = align(header + 8, 4)
|
|
record_offset = physical_base_offset + 8
|
|
if record_offset + 8 > len(extent.data):
|
|
raise FixtureError("extent records lie outside the base image")
|
|
root_inode = extent.inode(extent.root_nid)
|
|
if not (
|
|
record_offset + 8 <= root_inode.offset
|
|
or root_inode.offset + root_inode.inode_size <= header
|
|
):
|
|
raise FixtureError("extent conversion overlaps the root inode")
|
|
struct.pack_into(
|
|
"<IHH", extent.data, header, 2, Z_EROFS_ADVISE_EXTENTS, 0
|
|
)
|
|
physical_base = UINT64_MAX - 4095
|
|
extent.put_u64(physical_base_offset, physical_base)
|
|
extent.put_u32(record_offset, 8192)
|
|
extent.put_u32(record_offset + 4, 4096)
|
|
extent.update_checksum()
|
|
extent_path = output / "extent-pa-wrap.erofs"
|
|
extent.save(extent_path)
|
|
cluster_size = 1 << (extent.block_bits + (extent.data[header + 7] & 15))
|
|
if extent.u32(header) != 2 or extent.u16(header + 6) != 0:
|
|
raise FixtureError("extent header does not declare exactly two records")
|
|
if extent.u16(header + 4) != Z_EROFS_ADVISE_EXTENTS:
|
|
raise FixtureError("extent advise does not select 4-byte records")
|
|
if cluster_size != 4096 or extent_inode.size <= cluster_size:
|
|
raise FixtureError("extent fixture lacks a second 4 KiB logical cluster")
|
|
if extent.u64(physical_base_offset) + extent.u32(record_offset) <= UINT64_MAX:
|
|
raise FixtureError("extent fixture does not overflow pa + plen")
|
|
evidence.append(
|
|
"extent-wrap "
|
|
f"nid={extent_inode.nid} inode_offset={extent_inode.offset} "
|
|
f"header={header} base_offset={physical_base_offset} "
|
|
f"record_offset={record_offset} recsize=4 pa=0x{physical_base:016x} "
|
|
"plen0=8192 plen1=4096 logical_probe=4096 overflow=yes"
|
|
)
|
|
manifest["extent_wrap"] = {
|
|
"path": "/extent.bin",
|
|
"nid": extent_inode.nid,
|
|
"inode_offset": extent_inode.offset,
|
|
"header_offset": header,
|
|
"physical_base_offset": physical_base_offset,
|
|
"record_offset": record_offset,
|
|
"record_size": 4,
|
|
"cluster_size": cluster_size,
|
|
"physical_base": physical_base,
|
|
"plen": [8192, 4096],
|
|
"logical_probe": 4096,
|
|
}
|
|
|
|
compact_epoch_range = nfs_a.clone()
|
|
range_inode = compact_epoch_range.resolve_root_entry("zz-identity.txt")
|
|
compact_epoch_range.put_u64(SUPER + 24, INT64_MAX)
|
|
compact_epoch_range.put_u32(range_inode.offset + 12, 1)
|
|
compact_epoch_range.update_checksum()
|
|
compact_epoch_range_path = output / "compact-epoch-range.erofs"
|
|
compact_epoch_range.save(compact_epoch_range_path)
|
|
|
|
compact_epoch_wrap = nfs_a.clone()
|
|
wrap_root = compact_epoch_wrap.inode(compact_epoch_wrap.root_nid)
|
|
compact_epoch_wrap.put_u64(SUPER + 24, UINT64_MAX)
|
|
compact_epoch_wrap.put_u32(wrap_root.offset + 12, 1)
|
|
compact_epoch_wrap.update_checksum()
|
|
compact_epoch_wrap_path = output / "compact-epoch-wrap.erofs"
|
|
compact_epoch_wrap.save(compact_epoch_wrap_path)
|
|
|
|
compact_nsec = nfs_a.clone()
|
|
compact_nsec.put_u32(SUPER + 32, 1_000_000_000)
|
|
compact_nsec.update_checksum()
|
|
compact_nsec_path = output / "compact-nsec-invalid.erofs"
|
|
compact_nsec.save(compact_nsec_path)
|
|
|
|
extended_base = ErofsImage.load(extended_base_path)
|
|
extended_inode = extended_base.resolve_root_entry("extended-time.txt")
|
|
if extended_inode.inode_size != 64:
|
|
raise FixtureError("extended timestamp target is not extended")
|
|
extended_nsec = extended_base.clone()
|
|
extended_nsec_inode = extended_nsec.resolve_root_entry("extended-time.txt")
|
|
extended_nsec.put_u32(extended_nsec_inode.offset + 40, 1_000_000_000)
|
|
extended_nsec.update_checksum()
|
|
extended_nsec_path = output / "extended-nsec-invalid.erofs"
|
|
extended_nsec.save(extended_nsec_path)
|
|
|
|
extended_seconds = extended_base.clone()
|
|
extended_seconds_inode = extended_seconds.resolve_root_entry(
|
|
"extended-time.txt"
|
|
)
|
|
extended_seconds.put_u64(extended_seconds_inode.offset + 32, INT64_MAX + 1)
|
|
extended_seconds.update_checksum()
|
|
extended_seconds_path = output / "extended-seconds-range.erofs"
|
|
extended_seconds.save(extended_seconds_path)
|
|
|
|
if compact_epoch_range.u64(SUPER + 24) + compact_epoch_range.u32(
|
|
range_inode.offset + 12
|
|
) != INT64_MAX + 1:
|
|
raise FixtureError("compact range fixture fields do not self-check")
|
|
if compact_epoch_wrap.u64(SUPER + 24) + compact_epoch_wrap.u32(
|
|
wrap_root.offset + 12
|
|
) <= UINT64_MAX:
|
|
raise FixtureError("compact wrap fixture fields do not overflow")
|
|
if compact_nsec.u32(SUPER + 32) != 1_000_000_000:
|
|
raise FixtureError("compact nanoseconds fixture is incorrect")
|
|
if extended_nsec.u32(extended_nsec_inode.offset + 40) != 1_000_000_000:
|
|
raise FixtureError("extended nanoseconds fixture is incorrect")
|
|
if extended_seconds.u64(extended_seconds_inode.offset + 32) != INT64_MAX + 1:
|
|
raise FixtureError("extended seconds fixture is incorrect")
|
|
|
|
evidence.extend(
|
|
[
|
|
"compact-epoch-range "
|
|
f"nid={range_inode.nid} inode_offset={range_inode.offset} "
|
|
f"epoch={INT64_MAX} delta=1 sum={INT64_MAX + 1}",
|
|
"compact-epoch-wrap "
|
|
f"root_nid={wrap_root.nid} inode_offset={wrap_root.offset} "
|
|
f"epoch={UINT64_MAX} delta=1 overflow=yes",
|
|
"compact-nsec "
|
|
f"fixed_nsec={compact_nsec.u32(SUPER + 32)}",
|
|
"extended-nsec "
|
|
f"nid={extended_nsec_inode.nid} inode_offset={extended_nsec_inode.offset} "
|
|
f"nsec={extended_nsec.u32(extended_nsec_inode.offset + 40)}",
|
|
"extended-seconds "
|
|
f"nid={extended_seconds_inode.nid} "
|
|
f"inode_offset={extended_seconds_inode.offset} "
|
|
f"seconds={extended_seconds.u64(extended_seconds_inode.offset + 32)}",
|
|
]
|
|
)
|
|
manifest["timestamps"] = {
|
|
"compact_path": "/zz-identity.txt",
|
|
"compact_nid": range_inode.nid,
|
|
"extended_path": "/extended-time.txt",
|
|
"extended_nid": extended_inode.nid,
|
|
"time_t_max": INT64_MAX,
|
|
"nsec_limit": 1_000_000_000,
|
|
}
|
|
|
|
for base in (nfs_base_path, extent_base_path, extended_base_path):
|
|
base.unlink()
|
|
shutil.rmtree(source)
|
|
(output / "fixture-evidence.txt").write_text(
|
|
"\n".join(evidence) + "\n", encoding="ascii"
|
|
)
|
|
(output / "fixture-manifest.json").write_text(
|
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
|
)
|
|
with (output / "SHA256SUMS").open("w", encoding="ascii") as sums:
|
|
for path in sorted(output.glob("*.erofs")):
|
|
sums.write(f"{sha256(path)} {path.name}\n")
|
|
print((output / "fixture-evidence.txt").read_text(encoding="ascii"), end="")
|
|
print((output / "SHA256SUMS").read_text(encoding="ascii"), end="")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
make_parser = subparsers.add_parser("make")
|
|
make_parser.add_argument("--output", type=Path, required=True)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
if args.command == "make":
|
|
make_fixtures(args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except (FixtureError, OSError, subprocess.CalledProcessError) as error:
|
|
raise SystemExit(f"review_fixtures.py: {error}") from error
|