662 lines
26 KiB
Python
Executable File
662 lines
26 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
from typing import Any
|
|
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
TESTS_DIR = SCRIPT_DIR.parent.parent
|
|
sys.path.insert(0, str(TESTS_DIR))
|
|
|
|
from erofs_fixture import ErofsImage, FEATURE_INCOMPAT_48BIT, SUPER
|
|
|
|
|
|
BLOCK_SIZE = 4096
|
|
CHUNK_FORMAT_INDEXES = 0x0020
|
|
CHUNK_FORMAT_48BIT = 0x0040
|
|
CHUNK_FORMAT_ALL = 0x007F
|
|
FEATURE_INCOMPAT_CHUNKED_FILE = 0x00000004
|
|
EROFS_I_DOT_OMITTED_BIT = 4
|
|
EROFS_I_NLINK_1_BIT = 5
|
|
EROFS_FT_REG_FILE = 1
|
|
EROFS_FT_DIR = 2
|
|
EROFS_FT_CHRDEV = 3
|
|
EROFS_FT_SYMLINK = 7
|
|
UUID = "67360174-0014-0000-0000-000000000174"
|
|
TARGET = b"target.txt"
|
|
TARGET_CONTENT = b"B14 target content\n"
|
|
ENTRY_CONTENT = b"B14 directory entry\n"
|
|
|
|
|
|
class FixtureError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def run(command: list[str], *, capture: bool = False) -> str:
|
|
result = subprocess.run(
|
|
command,
|
|
check=False,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
if result.returncode != 0:
|
|
raise FixtureError(
|
|
f"command failed ({result.returncode}): {' '.join(command)}\n{result.stdout}"
|
|
)
|
|
return result.stdout if capture else ""
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def align_up(value: int, alignment: int) -> int:
|
|
return (value + alignment - 1) & ~(alignment - 1)
|
|
|
|
|
|
def source_bytes(label: str, size: int) -> bytes:
|
|
chunks = []
|
|
for index in range(math.ceil(size / 32)):
|
|
chunks.append(hashlib.sha256(f"B14:{label}:{index}".encode("ascii")).digest())
|
|
return b"".join(chunks)[:size]
|
|
|
|
|
|
def make_source(root: Path) -> None:
|
|
real_dir = root / "real-dir"
|
|
real_dir.mkdir(parents=True)
|
|
(real_dir / "entry.txt").write_bytes(ENTRY_CONTENT)
|
|
(root / "target.txt").write_bytes(TARGET_CONTENT)
|
|
(root / "chunk-dir").write_bytes(source_bytes("chunk-dir", 8192))
|
|
link_data = TARGET + source_bytes("chunk-link", 8192 - len(TARGET))
|
|
(root / "chunk-link").write_bytes(link_data)
|
|
(root / "chunk-char").write_bytes(source_bytes("chunk-char", 8192))
|
|
|
|
|
|
def make_image(mkfs: str, primary: Path, blob: Path, source: Path) -> None:
|
|
run(
|
|
[
|
|
mkfs,
|
|
"-T0",
|
|
"--all-time",
|
|
"--all-root",
|
|
"--workers=1",
|
|
f"-U{UUID}",
|
|
"--chunksize=4096",
|
|
f"--blobdev={blob}",
|
|
str(primary),
|
|
str(source),
|
|
]
|
|
)
|
|
|
|
|
|
def root_entry(image: ErofsImage, path: str):
|
|
_, entry = image.resolve_root_entry(path)
|
|
return entry
|
|
|
|
|
|
def inode_data(image: ErofsImage, inode) -> bytes:
|
|
if inode.layout == 0:
|
|
offset = inode.start_block << image.block_bits
|
|
elif inode.layout == 2:
|
|
offset = inode.offset + inode.inode_size + inode.xattr_size
|
|
else:
|
|
raise FixtureError(f"unsupported source data layout {inode.layout}")
|
|
end = offset + inode.size
|
|
if end > len(image.data):
|
|
raise FixtureError("source inode data exceeds primary image")
|
|
return bytes(image.data[offset:end])
|
|
|
|
|
|
def chunk_info(image: ErofsImage, path: str) -> dict[str, int]:
|
|
entry = root_entry(image, 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)
|
|
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 entry_size == 8:
|
|
high, device_id, low = struct.unpack_from("<HHI", image.data, index_base)
|
|
else:
|
|
high, device_id = 0, 0
|
|
low = struct.unpack_from("<I", image.data, index_base)[0]
|
|
return {
|
|
"nid": inode.nid,
|
|
"inode_offset": inode.offset,
|
|
"inode_size": inode.inode_size,
|
|
"format": chunk_format,
|
|
"reserved": reserved,
|
|
"index_base": index_base,
|
|
"entry_size": entry_size,
|
|
"high": high,
|
|
"device_id": device_id,
|
|
"low": low,
|
|
"size": inode.size,
|
|
"mode": inode.mode,
|
|
}
|
|
|
|
|
|
def patch_root_type(image: ErofsImage, path: str, file_type: int) -> None:
|
|
entry = root_entry(image, path)
|
|
offset = entry.offset + 10
|
|
if image.data[offset] != EROFS_FT_REG_FILE:
|
|
raise FixtureError(f"{path}: root entry is not a regular file")
|
|
image.data[offset] = file_type
|
|
|
|
|
|
def patch_mode(image: ErofsImage, path: str, mode_type: int) -> None:
|
|
info = chunk_info(image, path)
|
|
if stat.S_IFMT(info["mode"]) != stat.S_IFREG:
|
|
raise FixtureError(f"{path}: carrier inode is not regular")
|
|
image.put_u16(info["inode_offset"] + 4, mode_type | (info["mode"] & 0o7777))
|
|
|
|
|
|
def patch_size(image: ErofsImage, path: str, size: int) -> None:
|
|
info = chunk_info(image, path)
|
|
if info["inode_size"] == 32:
|
|
image.put_u32(info["inode_offset"] + 8, size)
|
|
else:
|
|
image.put_u64(info["inode_offset"] + 8, size)
|
|
|
|
|
|
def patch_directory(image: ErofsImage, blob: bytearray) -> None:
|
|
real_entry = root_entry(image, "/real-dir")
|
|
real_inode = image.inode(real_entry.nid)
|
|
directory_data = bytearray(inode_data(image, real_inode))
|
|
info = chunk_info(image, "/chunk-dir")
|
|
if info["device_id"] != 1 or info["high"] != 0:
|
|
raise FixtureError("chunk directory carrier is not on external slot 1")
|
|
blob_offset = info["low"] << image.block_bits
|
|
if blob_offset + image.block_size > len(blob):
|
|
raise FixtureError("chunk directory carrier exceeds blob")
|
|
first_name_offset = struct.unpack_from("<H", directory_data, 8)[0]
|
|
for index in range(first_name_offset // 12):
|
|
entry_offset = index * 12
|
|
name_offset = struct.unpack_from("<H", directory_data, entry_offset + 8)[0]
|
|
end_offset = (
|
|
struct.unpack_from("<H", directory_data, entry_offset + 20)[0]
|
|
if index + 1 < first_name_offset // 12
|
|
else len(directory_data)
|
|
)
|
|
name = bytes(directory_data[name_offset:end_offset]).split(b"\0", 1)[0]
|
|
if name == b".":
|
|
struct.pack_into("<Q", directory_data, entry_offset, info["nid"])
|
|
elif name == b"..":
|
|
struct.pack_into("<Q", directory_data, entry_offset, image.root_nid)
|
|
blob[blob_offset : blob_offset + image.block_size] = b"\0" * image.block_size
|
|
blob[blob_offset : blob_offset + len(directory_data)] = directory_data
|
|
patch_mode(image, "/chunk-dir", stat.S_IFDIR)
|
|
patch_size(image, "/chunk-dir", len(directory_data))
|
|
inode_offset = info["inode_offset"]
|
|
inode_format = image.u16(inode_offset)
|
|
inode_format &= ~((1 << EROFS_I_DOT_OMITTED_BIT) | (1 << EROFS_I_NLINK_1_BIT))
|
|
inode_format |= real_inode.inode_format & (1 << EROFS_I_DOT_OMITTED_BIT)
|
|
image.put_u16(inode_offset, inode_format)
|
|
if info["inode_size"] == 32:
|
|
image.put_u16(inode_offset + 6, 2)
|
|
else:
|
|
image.put_u32(inode_offset + 40, 2)
|
|
patch_root_type(image, "/chunk-dir", EROFS_FT_DIR)
|
|
|
|
|
|
def patch_symlink(image: ErofsImage) -> None:
|
|
patch_mode(image, "/chunk-link", stat.S_IFLNK)
|
|
patch_size(image, "/chunk-link", len(TARGET))
|
|
patch_root_type(image, "/chunk-link", EROFS_FT_SYMLINK)
|
|
|
|
|
|
def patch_character(image: ErofsImage) -> None:
|
|
patch_mode(image, "/chunk-char", stat.S_IFCHR)
|
|
patch_size(image, "/chunk-char", 0)
|
|
patch_root_type(image, "/chunk-char", EROFS_FT_CHRDEV)
|
|
|
|
|
|
def enable_48bit(image: ErofsImage) -> None:
|
|
if image.feature_incompat & FEATURE_INCOMPAT_48BIT:
|
|
raise FixtureError("48-bit feature already enabled")
|
|
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)
|
|
for path in ("/chunk-dir", "/chunk-link", "/chunk-char"):
|
|
info = chunk_info(image, path)
|
|
if info["entry_size"] != 8 or info["high"] != 0:
|
|
raise FixtureError(f"{path}: cannot promote chunk index to 48-bit")
|
|
image.put_u16(info["inode_offset"] + 16, info["format"] | CHUNK_FORMAT_48BIT)
|
|
|
|
|
|
def save_image(image: ErofsImage, path: Path) -> None:
|
|
if image.feature_compat & 1:
|
|
image.update_checksum()
|
|
image.validate_superblock()
|
|
image.save(path)
|
|
|
|
|
|
def patch_index_low(image: ErofsImage, path: str, value: int) -> None:
|
|
info = chunk_info(image, path)
|
|
if info["entry_size"] != 8:
|
|
raise FixtureError("bad-index fixture requires an indexed chunk")
|
|
image.put_u32(info["index_base"] + 4, value)
|
|
|
|
|
|
def patch_index48(image: ErofsImage, path: str, high: int, low: int) -> None:
|
|
info = chunk_info(image, path)
|
|
if info["entry_size"] != 8 or not info["format"] & CHUNK_FORMAT_48BIT:
|
|
raise FixtureError("bad-index48 fixture requires a 48-bit chunk index")
|
|
image.put_u16(info["index_base"], high)
|
|
image.put_u32(info["index_base"] + 4, low)
|
|
|
|
|
|
def load_spec(path: Path) -> dict[str, Any]:
|
|
spec = json.loads(path.read_text(encoding="ascii"))
|
|
if spec.get("schema") != 1 or spec.get("batch") != "B14":
|
|
raise FixtureError("invalid B14 fixture schema")
|
|
if spec.get("candidate") != "P15-056" or spec.get("test") != "TC174-chunk-nonregular":
|
|
raise FixtureError("invalid B14 fixture identity")
|
|
cases = spec.get("cases")
|
|
if not isinstance(cases, list) or len(cases) != 10:
|
|
raise FixtureError("B14 fixture case denominator changed")
|
|
identifiers = [case.get("id") for case in cases]
|
|
if len(set(identifiers)) != len(identifiers) or not all(isinstance(value, str) for value in identifiers):
|
|
raise FixtureError("invalid or duplicate B14 case ID")
|
|
return spec
|
|
|
|
|
|
def write_sums(output: Path, names: list[str]) -> dict[str, str]:
|
|
hashes = {name: sha256(output / name) for name in sorted(names)}
|
|
with (output / "SHA256SUMS").open("w", encoding="ascii") as stream:
|
|
for name, digest in hashes.items():
|
|
stream.write(f"{digest} {name}\n")
|
|
return hashes
|
|
|
|
|
|
def generate(args: argparse.Namespace) -> None:
|
|
output = args.output
|
|
if output.exists():
|
|
raise FixtureError(f"output already exists: {output}")
|
|
spec = load_spec(args.spec)
|
|
output.mkdir(parents=True)
|
|
source = output / "source"
|
|
source.mkdir()
|
|
make_source(source)
|
|
raw_primary = output / "raw.erofs"
|
|
blob_path = output / "B14-slot1.blob"
|
|
blob_path.write_bytes(b"")
|
|
make_image(args.mkfs, raw_primary, blob_path, source)
|
|
|
|
image = ErofsImage.load(raw_primary)
|
|
image.validate_superblock()
|
|
if not image.feature_incompat & FEATURE_INCOMPAT_CHUNKED_FILE:
|
|
raise FixtureError("mkfs image lacks CHUNKED_FILE")
|
|
blob = bytearray(blob_path.read_bytes())
|
|
for path in ("/chunk-dir", "/chunk-link", "/chunk-char"):
|
|
info = chunk_info(image, path)
|
|
if info["entry_size"] != 8 or info["device_id"] != 1 or info["high"] != 0:
|
|
raise FixtureError(f"{path}: unexpected mkfs chunk carrier")
|
|
|
|
patch_directory(image, blob)
|
|
patch_symlink(image)
|
|
patch_character(image)
|
|
blob_path.write_bytes(blob)
|
|
good32 = image.clone()
|
|
save_image(good32, output / "B14-good32.erofs")
|
|
|
|
good48 = image.clone()
|
|
enable_48bit(good48)
|
|
save_image(good48, output / "B14-good48.erofs")
|
|
|
|
blob_blocks = len(blob) // BLOCK_SIZE
|
|
bad_index = good32.clone()
|
|
patch_index_low(bad_index, "/chunk-link", blob_blocks + 1)
|
|
save_image(bad_index, output / "B14-bad-index.erofs")
|
|
|
|
bad_index48 = good48.clone()
|
|
patch_index48(bad_index48, "/chunk-link", 0xFFFF, 0xFFFFFFFE)
|
|
save_image(bad_index48, output / "B14-bad-index48.erofs")
|
|
|
|
bad_reserved = good32.clone()
|
|
directory = chunk_info(bad_reserved, "/chunk-dir")
|
|
bad_reserved.put_u16(directory["inode_offset"] + 18, 1)
|
|
save_image(bad_reserved, output / "B14-bad-reserved.erofs")
|
|
|
|
bad_format = good32.clone()
|
|
link = chunk_info(bad_format, "/chunk-link")
|
|
bad_format.put_u16(link["inode_offset"] + 16, link["format"] | (CHUNK_FORMAT_ALL + 1))
|
|
save_image(bad_format, output / "B14-bad-format.erofs")
|
|
|
|
bad_no_index = good48.clone()
|
|
link48 = chunk_info(bad_no_index, "/chunk-link")
|
|
bad_no_index.put_u16(
|
|
link48["inode_offset"] + 16,
|
|
(link48["format"] | CHUNK_FORMAT_48BIT) & ~CHUNK_FORMAT_INDEXES,
|
|
)
|
|
save_image(bad_no_index, output / "B14-bad-48-no-index.erofs")
|
|
raw_primary.unlink()
|
|
|
|
names = [
|
|
"B14-slot1.blob",
|
|
"B14-good32.erofs",
|
|
"B14-good48.erofs",
|
|
"B14-bad-index.erofs",
|
|
"B14-bad-index48.erofs",
|
|
"B14-bad-reserved.erofs",
|
|
"B14-bad-format.erofs",
|
|
"B14-bad-48-no-index.erofs",
|
|
]
|
|
hashes = write_sums(output, names)
|
|
manifest = {
|
|
"schema": 1,
|
|
"batch": "B14",
|
|
"candidate": "P15-056",
|
|
"test": "TC174-chunk-nonregular",
|
|
"mkfs_version": run([args.mkfs, "-V"], capture=True).splitlines()[0],
|
|
"hashes": hashes,
|
|
"blob_blocks": blob_blocks,
|
|
"target": TARGET.decode("ascii"),
|
|
"qualification": {
|
|
"good32": "fsck.erofs plus independent parser",
|
|
"good48": "independent parser; erofs-utils 1.8.6 lacks 48-bit support",
|
|
},
|
|
"cases": [case["id"] for case in spec["cases"]],
|
|
"good32": {
|
|
path: chunk_info(good32, path)
|
|
for path in ("/chunk-dir", "/chunk-link", "/chunk-char")
|
|
},
|
|
"good48": {
|
|
path: chunk_info(good48, path)
|
|
for path in ("/chunk-dir", "/chunk-link", "/chunk-char")
|
|
},
|
|
}
|
|
(output / "manifest.json").write_text(
|
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
|
)
|
|
|
|
|
|
def verify(args: argparse.Namespace) -> None:
|
|
spec = load_spec(args.spec)
|
|
output = args.output
|
|
manifest = json.loads((output / "manifest.json").read_text(encoding="ascii"))
|
|
if manifest.get("cases") != [case["id"] for case in spec["cases"]]:
|
|
raise FixtureError("manifest case list differs from B14 spec")
|
|
for name, expected in manifest.get("hashes", {}).items():
|
|
if sha256(output / name) != expected:
|
|
raise FixtureError(f"fixture hash mismatch: {name}")
|
|
|
|
good32 = ErofsImage.load(output / "B14-good32.erofs")
|
|
good48 = ErofsImage.load(output / "B14-good48.erofs")
|
|
blob = (output / "B14-slot1.blob").read_bytes()
|
|
for image, width in ((good32, 32), (good48, 48)):
|
|
image.validate_superblock()
|
|
directory = chunk_info(image, "/chunk-dir")
|
|
link = chunk_info(image, "/chunk-link")
|
|
character = chunk_info(image, "/chunk-char")
|
|
for info in (directory, link, character):
|
|
if info["device_id"] != 1 or info["entry_size"] != 8:
|
|
raise FixtureError(f"{width}-bit external chunk contract changed")
|
|
has_48bit = bool(info["format"] & CHUNK_FORMAT_48BIT)
|
|
if has_48bit != (width == 48) or info["high"] != 0:
|
|
raise FixtureError(f"{width}-bit chunk format contract changed")
|
|
if stat.S_IFMT(directory["mode"]) != stat.S_IFDIR:
|
|
raise FixtureError("chunk directory mode changed")
|
|
if stat.S_IFMT(link["mode"]) != stat.S_IFLNK or link["size"] != len(TARGET):
|
|
raise FixtureError("chunk symlink mode or size changed")
|
|
if stat.S_IFMT(character["mode"]) != stat.S_IFCHR or character["size"] != 0:
|
|
raise FixtureError("chunk character mode or size changed")
|
|
link_offset = link["low"] << image.block_bits
|
|
if blob[link_offset : link_offset + len(TARGET)] != TARGET:
|
|
raise FixtureError("chunk symlink payload changed")
|
|
directory_offset = directory["low"] << image.block_bits
|
|
directory_data = blob[directory_offset : directory_offset + directory["size"]]
|
|
first_name_offset = struct.unpack_from("<H", directory_data, 8)[0]
|
|
names = []
|
|
nids = {}
|
|
for index in range(first_name_offset // 12):
|
|
entry_offset = index * 12
|
|
name_offset = struct.unpack_from("<H", directory_data, entry_offset + 8)[0]
|
|
end_offset = (
|
|
struct.unpack_from("<H", directory_data, entry_offset + 20)[0]
|
|
if index + 1 < first_name_offset // 12
|
|
else len(directory_data)
|
|
)
|
|
name = bytes(directory_data[name_offset:end_offset]).split(b"\0", 1)[0]
|
|
names.append(name)
|
|
nids[name] = struct.unpack_from("<Q", directory_data, entry_offset)[0]
|
|
if b"entry.txt" not in names or nids.get(b".", directory["nid"]) != directory["nid"]:
|
|
raise FixtureError("chunk directory payload identity changed")
|
|
|
|
bad_index = ErofsImage.load(output / "B14-bad-index.erofs")
|
|
if chunk_info(bad_index, "/chunk-link")["low"] <= manifest["blob_blocks"]:
|
|
raise FixtureError("32-bit bad index remains inside external device")
|
|
bad_index48 = chunk_info(ErofsImage.load(output / "B14-bad-index48.erofs"), "/chunk-link")
|
|
if bad_index48["high"] != 0xFFFF or bad_index48["low"] != 0xFFFFFFFE:
|
|
raise FixtureError("48-bit bad index boundary changed")
|
|
if chunk_info(ErofsImage.load(output / "B14-bad-reserved.erofs"), "/chunk-dir")["reserved"] != 1:
|
|
raise FixtureError("reserved-field mutation changed")
|
|
if not chunk_info(ErofsImage.load(output / "B14-bad-format.erofs"), "/chunk-link")["format"] & 0x80:
|
|
raise FixtureError("unsupported-format mutation changed")
|
|
no_index = chunk_info(ErofsImage.load(output / "B14-bad-48-no-index.erofs"), "/chunk-link")
|
|
if not no_index["format"] & CHUNK_FORMAT_48BIT or no_index["format"] & CHUNK_FORMAT_INDEXES:
|
|
raise FixtureError("48-bit-without-indexes mutation changed")
|
|
|
|
run(
|
|
[
|
|
args.fsck,
|
|
f"--device={output / 'B14-slot1.blob'}",
|
|
str(output / "B14-good32.erofs"),
|
|
]
|
|
)
|
|
|
|
|
|
def extract_function(source: str, name: str) -> str:
|
|
match = re.search(
|
|
r"^(?:[A-Za-z_][^\n;{}]*\s+)?" + re.escape(name) + r"\s*\(",
|
|
source,
|
|
re.MULTILINE,
|
|
)
|
|
if not match:
|
|
raise FixtureError(f"missing function: {name}")
|
|
start = source.rfind("\n\n", 0, match.start()) + 2
|
|
brace = source.find("{", match.end())
|
|
depth = 0
|
|
state = "code"
|
|
index = brace
|
|
while index < len(source):
|
|
char = source[index]
|
|
following = source[index + 1] if index + 1 < len(source) else ""
|
|
if state == "code":
|
|
if char == "/" and following == "*":
|
|
state = "block"
|
|
index += 2
|
|
continue
|
|
if char == "/" and following == "/":
|
|
state = "line"
|
|
index += 2
|
|
continue
|
|
if char == '"':
|
|
state = "string"
|
|
elif char == "'":
|
|
state = "character"
|
|
elif char == "{":
|
|
depth += 1
|
|
elif char == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return source[start : index + 1]
|
|
elif state == "block" and char == "*" and following == "/":
|
|
state = "code"
|
|
index += 2
|
|
continue
|
|
elif state == "line" and char == "\n":
|
|
state = "code"
|
|
elif state in {"string", "character"}:
|
|
if char == "\\":
|
|
index += 2
|
|
continue
|
|
if (state == "string" and char == '"') or (state == "character" and char == "'"):
|
|
state = "code"
|
|
index += 1
|
|
raise FixtureError(f"unterminated function: {name}")
|
|
|
|
|
|
def committed(root: Path, baseline: str, path: str) -> str:
|
|
result = subprocess.run(
|
|
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
|
|
check=False,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
if result.returncode != 0:
|
|
raise FixtureError(f"cannot read B14 baseline {path}: {result.stderr}")
|
|
return result.stdout
|
|
|
|
|
|
def audit(args: argparse.Namespace) -> None:
|
|
load_spec(args.spec)
|
|
root = args.root
|
|
dut = args.dut
|
|
inode = (dut / "src/inode.c").read_text(encoding="utf-8")
|
|
data = (dut / "src/data.c").read_text(encoding="utf-8")
|
|
vnops = (dut / "src/erofs_vnops.c").read_text(encoding="utf-8")
|
|
linux_inode = (root / "src-linux/inode.c").read_text(encoding="utf-8")
|
|
base_inode = committed(root, args.baseline, "src/inode.c")
|
|
base_data = committed(root, args.baseline, "src/data.c")
|
|
old_gate = "\t\tif (!erofs_sb_has_chunked_file(sbi) || vi->vtype != VREG) {"
|
|
new_gate = "\t\tif (!erofs_sb_has_chunked_file(sbi)) {"
|
|
if base_inode.count(old_gate) != 1:
|
|
raise FixtureError("B14 baseline type gate changed")
|
|
expected_inode = base_inode.replace(old_gate, new_gate, 1)
|
|
if inode != expected_inode:
|
|
raise FixtureError("inode.c differs from the single declared B14 transform")
|
|
if data != base_data:
|
|
raise FixtureError("data.c changed although the chunk mapper is already type-neutral")
|
|
|
|
read_inode = extract_function(inode, "erofs_read_inode")
|
|
chunk_mapper = extract_function(data, "erofs_map_blocks_chunk")
|
|
map_device = extract_function(data, "erofs_map_dev")
|
|
open_vnode = extract_function(vnops, "erofs_open")
|
|
read_vnode = extract_function(vnops, "erofs_read")
|
|
readdir_vnode = extract_function(vnops, "erofs_readdir")
|
|
readlink_vnode = extract_function(vnops, "erofs_readlink")
|
|
linux_read_inode = extract_function(linux_inode, "erofs_read_inode")
|
|
|
|
required_inode = (
|
|
"!erofs_sb_has_chunked_file(sbi)",
|
|
"le16toh(chunk_info.reserved) != 0",
|
|
"vi->chunkformat & ~EROFS_CHUNK_FORMAT_ALL",
|
|
"EROFS_CHUNK_FORMAT_48BIT",
|
|
"EROFS_CHUNK_FORMAT_INDEXES",
|
|
"vi->chunkbits >= 64",
|
|
"case VREG:",
|
|
"case VDIR:",
|
|
"case VLNK:",
|
|
)
|
|
for marker in required_inode:
|
|
if marker not in read_inode:
|
|
raise FixtureError(f"B14 removed inode invariant: {marker}")
|
|
if "vi->vtype != VREG" in read_inode:
|
|
raise FixtureError("nonregular chunk type gate remains")
|
|
if read_inode.count("return (EINTEGRITY);") < 8 or "return (EOPNOTSUPP);" not in read_inode:
|
|
raise FixtureError("FreeBSD inode errno contract changed")
|
|
|
|
required_mapper = (
|
|
"vi->chunkbits",
|
|
"vi->chunkformat",
|
|
"vi->inode_off",
|
|
"vi->inode_isize",
|
|
"vi->xattr_isize",
|
|
"map->m_deviceid = raw_device_id & sbi->device_id_mask",
|
|
"map->m_flags |= EROFS_MAP_MAPPED",
|
|
)
|
|
for marker in required_mapper:
|
|
if marker not in chunk_mapper:
|
|
raise FixtureError(f"chunk mapper contract changed: {marker}")
|
|
if "vtype" in chunk_mapper or "VREG" in chunk_mapper or "VLNK" in chunk_mapper or "VDIR" in chunk_mapper:
|
|
raise FixtureError("chunk mapper acquired an OS vnode type dependency")
|
|
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", chunk_mapper + map_device):
|
|
raise FixtureError("negative Linux errno entered the FreeBSD map path")
|
|
for marker in ("ENODEV", "dif->devvp", "dif->cp", "erofs_check_device_range"):
|
|
if marker not in map_device:
|
|
raise FixtureError(f"FreeBSD device/GEOM boundary changed: {marker}")
|
|
|
|
if "VN_ISDEV(vp)" not in open_vnode or "return (EOPNOTSUPP);" not in open_vnode:
|
|
raise FixtureError("special vnode open safety changed")
|
|
if "case VREG:" not in read_vnode or "case VDIR:" not in read_vnode:
|
|
raise FixtureError("FreeBSD VOP_READ type boundary changed")
|
|
if "v_type != VDIR" not in readdir_vnode or "v_type != VLNK" not in readlink_vnode:
|
|
raise FixtureError("FreeBSD readdir/readlink type boundary changed")
|
|
|
|
for marker in ("case S_IFDIR:", "case S_IFREG:", "case S_IFLNK:"):
|
|
if marker not in linux_read_inode:
|
|
raise FixtureError(f"Linux common data-layout type marker changed: {marker}")
|
|
linux_chunk = linux_read_inode[linux_read_inode.index("if (vi->datalayout == EROFS_INODE_CHUNK_BASED)") :]
|
|
if "vi->chunkformat" not in linux_chunk or "S_ISREG" in linux_chunk.split("inode_set_atime", 1)[0]:
|
|
raise FixtureError("Linux chunk summary is no longer type-neutral")
|
|
|
|
report = {
|
|
"schema": 1,
|
|
"batch": "B14",
|
|
"candidate": "P15-056",
|
|
"baseline": args.baseline,
|
|
"source_transform_count": 1,
|
|
"data_c_unchanged": True,
|
|
"linux_common_types": ["S_IFDIR", "S_IFREG", "S_IFLNK"],
|
|
"freebsd_vnode_types": ["VDIR", "VREG", "VLNK"],
|
|
"freebsd_boundaries": [
|
|
"positive errno",
|
|
"explicit GEOM device slots",
|
|
"special vnode open EOPNOTSUPP",
|
|
"typed readdir/readlink VOPs",
|
|
],
|
|
}
|
|
args.report.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser()
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
generate_parser = subparsers.add_parser("generate")
|
|
generate_parser.add_argument("--output", type=Path, required=True)
|
|
generate_parser.add_argument("--spec", type=Path, required=True)
|
|
generate_parser.add_argument("--mkfs", default="mkfs.erofs")
|
|
verify_parser = subparsers.add_parser("verify")
|
|
verify_parser.add_argument("--output", type=Path, required=True)
|
|
verify_parser.add_argument("--spec", type=Path, required=True)
|
|
verify_parser.add_argument("--fsck", default="fsck.erofs")
|
|
audit_parser = subparsers.add_parser("audit")
|
|
audit_parser.add_argument("--root", type=Path, required=True)
|
|
audit_parser.add_argument("--dut", type=Path, required=True)
|
|
audit_parser.add_argument("--baseline", required=True)
|
|
audit_parser.add_argument("--spec", type=Path, required=True)
|
|
audit_parser.add_argument("--report", type=Path, required=True)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
if args.command == "generate":
|
|
generate(args)
|
|
elif args.command == "verify":
|
|
verify(args)
|
|
else:
|
|
audit(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|