Bring the decoder over from the project it grew in
Eighteen commits of history, grafted onto the scaffold rather than over it. It leaves behind everything that only meant something inside that project: paths under one machine's mount, references to a transcoder that is not here, and a benchmark defaulting to a tile nobody else has. One fixture is real terrain and stays, because it is the only small input that exercises the direct-nibble Huffman weight form. Its licence requires attribution to travel with it, so the README's acknowledgements and the corpus manifest both carry the Copernicus notice now. Everything else committed here is synthetic or was written by the zstd CLI, including the .hgtz container, whose name looks like a tile and whose samples are a function of their position.
This commit is contained in:
commit
d1caf353b3
3 changed files with 3624 additions and 2 deletions
399
tool/make_corpus.py
Normal file
399
tool/make_corpus.py
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Builds the committed test corpus in test/fixtures/corpus/.
|
||||
|
||||
Every frame here is either produced by libzstd (through the `zstd` CLI or
|
||||
pyzstd) or hand-built and then handed to pyzstd for a verdict, so "legal" and
|
||||
"malformed" are libzstd's opinion rather than this file's. The expected plain
|
||||
bytes of a legal frame are whatever pyzstd decompressed it to, gzipped so the
|
||||
committed fixture stays small; nothing here writes an expectation by hand.
|
||||
|
||||
manifest.json records, per case, the exact command that made the frame. The
|
||||
Dart tests read that manifest and never carry a literal of their own.
|
||||
|
||||
./tool/make_corpus.py # regenerates test/fixtures/corpus
|
||||
|
||||
Needs pyzstd, the zstd CLI, and for the one real-tile case a readable
|
||||
a directory of .hgtz containers, given in HGTZ_TILE.
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
import json
|
||||
import pathlib
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pyzstd
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
OUT = ROOT / "test" / "fixtures" / "corpus"
|
||||
# One fixture is a block of real terrain lifted out of an elevation
|
||||
# container. It is committed, so rebuilding it is optional: point HGTZ_TILE at
|
||||
# a .hgtz file to regenerate it, and without one that case is left alone.
|
||||
_TILE = os.environ.get("HGTZ_TILE")
|
||||
TILE = pathlib.Path(_TILE) if _TILE else None
|
||||
TILE_BLOCK = 183
|
||||
|
||||
MAGIC = struct.pack("<I", 0xFD2FB528)
|
||||
|
||||
cases = []
|
||||
plains = {}
|
||||
|
||||
|
||||
def plain(name, data):
|
||||
"""Records `data` as a gzipped expectation file, returning its name."""
|
||||
if name not in plains:
|
||||
plains[name] = data
|
||||
(OUT / (name + ".raw.gz")).write_bytes(gzip.compress(data, 9, mtime=0))
|
||||
elif plains[name] != data:
|
||||
raise SystemExit(f"{name}: two different expectations")
|
||||
return name + ".raw.gz"
|
||||
|
||||
|
||||
def legal(name, frame, parts, command, expected_size=None):
|
||||
"""A frame pyzstd accepts. `parts` name the gzipped expectation files."""
|
||||
want = b"".join(plains[p[: -len(".raw.gz")]] for p in parts)
|
||||
got = pyzstd.decompress(frame)
|
||||
if got != want:
|
||||
raise SystemExit(f"{name}: pyzstd gives {len(got)} bytes, wanted {len(want)}")
|
||||
(OUT / (name + ".zst")).write_bytes(frame)
|
||||
entry = {
|
||||
"name": name,
|
||||
"frame": name + ".zst",
|
||||
"command": command,
|
||||
"verdict": "legal",
|
||||
"plain": parts,
|
||||
"size": len(want),
|
||||
}
|
||||
if expected_size is not None:
|
||||
entry["expectedSize"] = expected_size
|
||||
cases.append(entry)
|
||||
|
||||
|
||||
def illegal(name, frame, command, note):
|
||||
"""A frame pyzstd refuses, or one no decoder should accept."""
|
||||
try:
|
||||
pyzstd.decompress(frame)
|
||||
except Exception as error:
|
||||
note = f"{note}; pyzstd refuses it: {type(error).__name__}"
|
||||
else:
|
||||
note = f"{note}; NOTE pyzstd accepts this one"
|
||||
(OUT / (name + ".zst")).write_bytes(frame)
|
||||
cases.append(
|
||||
{
|
||||
"name": name,
|
||||
"frame": name + ".zst",
|
||||
"command": command,
|
||||
"verdict": "refused",
|
||||
"why": note,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def cli(args, data, name="stdin"):
|
||||
"""Runs the zstd CLI. A file argument and a pipe differ: from a pipe the
|
||||
size is unknown, so the frame comes out windowed with no content size."""
|
||||
if name == "stdin":
|
||||
run = subprocess.run(
|
||||
["zstd", "-q", "-c"] + args,
|
||||
input=data,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
else:
|
||||
scratch = OUT / name
|
||||
scratch.write_bytes(data)
|
||||
run = subprocess.run(
|
||||
["zstd", "-q", "-c"] + args + [str(scratch)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
scratch.unlink()
|
||||
if run.returncode != 0:
|
||||
raise SystemExit(f"zstd {args}: {run.stderr.decode()}")
|
||||
return run.stdout
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# The plain inputs several cases share. A is incompressible, and pseudo-random
|
||||
# rather than random so that regenerating the corpus gives the same bytes.
|
||||
#
|
||||
# The same xorshift64 is written out again in test/corpus.dart, because the
|
||||
# three cases too large to commit build their input in the test and compress
|
||||
# it with the zstd CLI there.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
MASK64 = (1 << 64) - 1
|
||||
|
||||
|
||||
def xorshift(seed, count):
|
||||
state = seed
|
||||
out = bytearray(count)
|
||||
for i in range(count):
|
||||
state ^= (state << 13) & MASK64
|
||||
state ^= state >> 7
|
||||
state ^= (state << 17) & MASK64
|
||||
out[i] = (state >> 33) & 0xFF
|
||||
return bytes(out)
|
||||
|
||||
|
||||
A = xorshift(0x9E3779B97F4A7C15, 5000)
|
||||
B = b"the quick brown fox " * 500
|
||||
TEXT = b"the quick brown fox jumps over the lazy dog. " * 24000
|
||||
|
||||
|
||||
def build():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
for stale in OUT.glob("*"):
|
||||
stale.unlink()
|
||||
|
||||
a = plain("A", A)
|
||||
b = plain("B", B)
|
||||
|
||||
# 1. Two frames in one buffer. pyzstd returns A+B; a decoder that reads
|
||||
# one frame must refuse rather than return A and stop.
|
||||
legal(
|
||||
"twoframes",
|
||||
pyzstd.compress(A, 3) + pyzstd.compress(B, 3),
|
||||
[a, b],
|
||||
"python3: pyzstd.compress(A, 3) + pyzstd.compress(B, 3), "
|
||||
"A = 5000 xorshift64 bytes, B = b'the quick brown fox ' * 500",
|
||||
)
|
||||
cases[-1]["trailing"] = "a second complete frame"
|
||||
|
||||
# 2. A frame with a content checksum. The three malformed variants are
|
||||
# derived from this one in the test; pyzstd's verdict on each is
|
||||
# recorded here so nothing is asserted on faith.
|
||||
checked = cli(["-3", "--check"], A, "A.bin")
|
||||
legal("checksum_good", checked, [a], "zstd -q -c -3 --check A.bin")
|
||||
for name, label, variant in (
|
||||
(
|
||||
"checksum_flipped",
|
||||
"the last checksum byte flipped",
|
||||
checked[:-1] + bytes([checked[-1] ^ 0x01]),
|
||||
),
|
||||
(
|
||||
"checksum_zeroed",
|
||||
"all four checksum bytes zeroed",
|
||||
checked[:-4] + b"\0\0\0\0",
|
||||
),
|
||||
(
|
||||
"checksum_truncated",
|
||||
"the last two bytes cut",
|
||||
checked[:-2],
|
||||
),
|
||||
):
|
||||
try:
|
||||
pyzstd.decompress(variant)
|
||||
except Exception as error:
|
||||
verdict = f"pyzstd refuses it: {type(error).__name__}"
|
||||
else:
|
||||
verdict = "NOTE pyzstd accepts it"
|
||||
cases.append(
|
||||
{
|
||||
"name": name,
|
||||
"frame": "checksum_good.zst",
|
||||
"command": f"zstd -q -c -3 --check A.bin, then {label}",
|
||||
"verdict": "refused",
|
||||
"why": verdict,
|
||||
"derive": label,
|
||||
}
|
||||
)
|
||||
|
||||
# 3. Frame_Content_Size_flag = 3 declaring a size no machine will give.
|
||||
# Built by rewriting a real frame's descriptor, so everything after the
|
||||
# header is a frame libzstd wrote.
|
||||
base = pyzstd.compress(b"hello world", 3)
|
||||
assert base[4] >> 6 == 0 and (base[4] >> 5) & 1 == 1, base[4]
|
||||
for label, declared in (
|
||||
("fcs_max_u64", (1 << 64) - 1),
|
||||
("fcs_2p40", 1 << 40),
|
||||
):
|
||||
frame = base[:4] + bytes([(3 << 6) | (base[4] & 0x3F)])
|
||||
frame += declared.to_bytes(8, "little") + base[6:]
|
||||
illegal(
|
||||
label,
|
||||
frame,
|
||||
f"python3: pyzstd.compress(b'hello world', 3) with the descriptor "
|
||||
f"rewritten to Frame_Content_Size_flag=3 declaring {declared}",
|
||||
"a 27-byte frame asking for a size no allocator will give",
|
||||
)
|
||||
|
||||
# 3b. The same eight-byte content size on a frame that is legal, so the
|
||||
# branch is reached by something that decodes and not only by two
|
||||
# frames refused on the way past it.
|
||||
legal(
|
||||
"fcs8",
|
||||
rewrite_fcs8(pyzstd.compress(B, 3), len(B)),
|
||||
[b],
|
||||
"python3: pyzstd.compress(B, 3) with the descriptor rewritten to "
|
||||
"Frame_Content_Size_flag=3 and an 8-byte size",
|
||||
)
|
||||
|
||||
# 4. The 53-byte frame that walked fseDecodeInterleaved past _weights[255].
|
||||
# Hand-built: a compressed block whose literals carry an FSE-coded
|
||||
# Huffman weight stream long enough to emit 257 weights.
|
||||
weights = bytes([0x10, 0x3F]) + bytes([0xAA] * 33) + bytes([0x02])
|
||||
body = bytes([0xA2, 0x00, 0x0A]) # compressed literals, 10 regenerated
|
||||
body += bytes([len(weights)]) + weights
|
||||
body += bytes([0x01, 0x01, 0x01, 0x00]) # sequence section, never reached
|
||||
frame = MAGIC + bytes([0x20, 0x0A]) + blockhdr(1, 2, len(body)) + body
|
||||
illegal(
|
||||
"huf_weight_overrun",
|
||||
frame,
|
||||
"hand-built: a compressed block whose Huffman weight description is "
|
||||
"[0x10, 0x3F] and whose FSE weight stream is 33 x 0xAA then 0x02",
|
||||
"the weight stream decodes past the 256th weight",
|
||||
)
|
||||
|
||||
# 5. RLE literals with Number_of_Sequences = 0, at every size format.
|
||||
# libzstd never emits these: it writes an RLE block instead.
|
||||
for fmt, count in ((0, 31), (1, 4000), (2, 70000), (2, 131072)):
|
||||
body = rle_literals(count, 0x5A, fmt)
|
||||
name = f"rlelit_fmt{fmt}_R{count}"
|
||||
expect = plain(name, bytes([0x5A]) * count)
|
||||
legal(
|
||||
name,
|
||||
single_frame([blockhdr(1, 2, len(body)) + body], count),
|
||||
[expect],
|
||||
f"hand-built: one compressed block, RLE literals Size_Format={fmt}, "
|
||||
f"Regenerated_Size={count}, byte 0x5A, Number_of_Sequences=0",
|
||||
)
|
||||
|
||||
# 6. RLE and raw blocks, which the real .hgtz corpus contains none of.
|
||||
content = b"\xab" * 5000 + b"hello world hello world hello wor" + b"\x07" * 40000
|
||||
assert len(content) == 5000 + 33 + 40000
|
||||
blocks = [
|
||||
blockhdr(0, 1, 5000) + b"\xab",
|
||||
blockhdr(0, 0, 33) + content[5000:5033],
|
||||
blockhdr(1, 1, 40000) + b"\x07",
|
||||
]
|
||||
expect = plain("mixed_rle_raw", content)
|
||||
legal(
|
||||
"mixed_rle_raw",
|
||||
single_frame(blocks, len(content)),
|
||||
[expect],
|
||||
"hand-built: [RLE 5000 x 0xAB][raw 33 bytes][RLE 40000 x 0x07] "
|
||||
"in one frame",
|
||||
)
|
||||
|
||||
# 7. No Frame_Content_Size at all: the shape the CLI writes from a pipe.
|
||||
frame = cli(["-3", "--no-check"], TEXT)
|
||||
assert frame[4] >> 6 == 0 and (frame[4] >> 5) & 1 == 0, frame[4]
|
||||
expect = plain("text", TEXT)
|
||||
legal(
|
||||
"nofcs_windowed",
|
||||
frame,
|
||||
[expect],
|
||||
"zstd -3 --no-check from stdin over "
|
||||
"b'the quick brown fox jumps over the lazy dog. ' * 24000",
|
||||
expected_size=len(TEXT),
|
||||
)
|
||||
|
||||
# 11. A real block that codes its Huffman weights as direct nibbles. The
|
||||
# synthetic shapes above never produce one, and the committed
|
||||
# container has none. Terrain is not ours to redistribute freely, so
|
||||
# the one committed here carries its licence's attribution in the
|
||||
# manifest; see the README's acknowledgements.
|
||||
if TILE is None:
|
||||
print("HGTZ_TILE not set: leaving direct_nibble as committed")
|
||||
else:
|
||||
tile = TILE.read_bytes()
|
||||
offset, length = struct.unpack_from(">II", tile, 12 + 8 * TILE_BLOCK)
|
||||
frame = bytes(tile[offset : offset + length])
|
||||
assert direct_nibble_weights(frame), "that block is not direct nibbles"
|
||||
expect = plain("direct_nibble", pyzstd.decompress(frame))
|
||||
legal(
|
||||
"direct_nibble",
|
||||
frame,
|
||||
[expect],
|
||||
f"block {TILE_BLOCK} of a Copernicus GLO-30 elevation tile "
|
||||
"transcoded into a .hgtz container, lifted out as its own frame; "
|
||||
"its literals carry the direct-nibble Huffman weight form",
|
||||
)
|
||||
|
||||
# 13. Dictionary_ID_flag set on a frame that carries no dictionary id.
|
||||
base = pyzstd.compress(A, 3)
|
||||
illegal(
|
||||
"dictflag",
|
||||
base[:4] + bytes([base[4] | 1]) + base[5:],
|
||||
"python3: pyzstd.compress(A, 3) with Dictionary_ID_flag forced to 1 "
|
||||
"in the frame header descriptor",
|
||||
"a dictionary this decoder does not have",
|
||||
)
|
||||
|
||||
(OUT / "manifest.json").write_text(json.dumps(cases, indent=2) + "\n")
|
||||
total = sum(f.stat().st_size for f in OUT.iterdir())
|
||||
print(f"{len(cases)} cases, {len(list(OUT.iterdir()))} files, {total} bytes")
|
||||
|
||||
|
||||
def rewrite_fcs8(frame, size):
|
||||
"""The same frame with an 8-byte Frame_Content_Size in place of its own."""
|
||||
descriptor = frame[4]
|
||||
single = (descriptor >> 5) & 1
|
||||
at = 5 + (0 if single else 1)
|
||||
width = {0: 1 if single else 0, 1: 2, 2: 4, 3: 8}[descriptor >> 6]
|
||||
return (
|
||||
frame[:4]
|
||||
+ bytes([(3 << 6) | (descriptor & 0x3F)])
|
||||
+ frame[5:at]
|
||||
+ size.to_bytes(8, "little")
|
||||
+ frame[at + width :]
|
||||
)
|
||||
|
||||
|
||||
def blockhdr(last, btype, size):
|
||||
return struct.pack("<I", (size << 3) | (btype << 1) | last)[:3]
|
||||
|
||||
|
||||
def rle_literals(count, byte, fmt):
|
||||
"""A compressed block body: RLE literals, then Number_of_Sequences = 0."""
|
||||
if fmt == 0:
|
||||
assert count < 32
|
||||
body = bytes([(count << 3) | (0 << 2) | 1, byte])
|
||||
elif fmt == 1:
|
||||
assert count < 4096
|
||||
body = bytes([((count & 0xF) << 4) | (1 << 2) | 1, count >> 4, byte])
|
||||
else:
|
||||
assert count < (1 << 20)
|
||||
body = bytes(
|
||||
[
|
||||
((count & 0xF) << 4) | (3 << 2) | 1,
|
||||
(count >> 4) & 0xFF,
|
||||
count >> 12,
|
||||
byte,
|
||||
]
|
||||
)
|
||||
return body + b"\x00"
|
||||
|
||||
|
||||
def single_frame(blocks, size):
|
||||
"""A single-segment frame declaring `size` and carrying `blocks`."""
|
||||
if size < 256:
|
||||
descriptor, field = (0 << 6) | (1 << 5), bytes([size])
|
||||
elif size < 65536 + 256:
|
||||
descriptor, field = (1 << 6) | (1 << 5), struct.pack("<H", size - 256)
|
||||
else:
|
||||
descriptor, field = (2 << 6) | (1 << 5), struct.pack("<I", size)
|
||||
return MAGIC + bytes([descriptor]) + field + b"".join(blocks)
|
||||
|
||||
|
||||
def direct_nibble_weights(frame):
|
||||
"""True when the frame's first block codes Huffman weights as nibbles."""
|
||||
descriptor = frame[4]
|
||||
at = 5 + (0 if (descriptor >> 5) & 1 else 1)
|
||||
at += {0: 1 if (descriptor >> 5) & 1 else 0, 1: 2, 2: 4, 3: 8}[descriptor >> 6]
|
||||
header = frame[at] | (frame[at + 1] << 8) | (frame[at + 2] << 16)
|
||||
at += 3
|
||||
if (header >> 1) & 3 != 2:
|
||||
return False
|
||||
literals = frame[at]
|
||||
if literals & 3 != 2:
|
||||
return False
|
||||
at += {0: 3, 1: 3, 2: 4, 3: 5}[(literals >> 2) & 3]
|
||||
return frame[at] >= 128
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(build())
|
||||
103
tool/oracle.py
Executable file
103
tool/oracle.py
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Check the Dart decoder against pyzstd over real .hgtz containers.
|
||||
|
||||
./tool/oracle.py <a .hgtz container>
|
||||
./tool/oracle.py --every 1500 --limit 8 <a directory of them>
|
||||
|
||||
pyzstd is the reference. Every block of every container named is extracted as
|
||||
its own frame, decompressed by pyzstd, and handed to tool/verify.dart, which
|
||||
decodes it again and compares byte for byte. One tile at a time, through a
|
||||
temporary directory that is deleted as it goes, because a whole tile's blocks
|
||||
decompress to around 29 MB.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pyzstd
|
||||
|
||||
HEADER = ">4sBbHHH"
|
||||
ENTRY = ">II"
|
||||
|
||||
|
||||
def frames(path: Path):
|
||||
"""Yields (index, frame bytes, expected decompressed size) per block."""
|
||||
data = path.read_bytes()
|
||||
magic, version, _level, rows, cols, block = struct.unpack_from(HEADER, data)
|
||||
if magic != b"HGTZ" or version != 1:
|
||||
raise SystemExit(f"{path}: not a version 1 .hgtz")
|
||||
block_cols = (cols + block - 1) // block
|
||||
block_rows = (rows + block - 1) // block
|
||||
for index in range(block_rows * block_cols):
|
||||
offset, length = struct.unpack_from(ENTRY, data, 12 + 8 * index)
|
||||
top = (index // block_cols) * block
|
||||
left = (index % block_cols) * block
|
||||
height = min(block, rows - top)
|
||||
width = min(block, cols - left)
|
||||
yield index, data[offset : offset + length], height * width * 2
|
||||
|
||||
|
||||
def check(path: Path, root: Path) -> bool:
|
||||
with tempfile.TemporaryDirectory() as name:
|
||||
directory = Path(name)
|
||||
count = 0
|
||||
for index, frame, expected in frames(path):
|
||||
plain = pyzstd.decompress(frame)
|
||||
if len(plain) != expected:
|
||||
raise SystemExit(
|
||||
f"{path} block {index}: pyzstd gave {len(plain)},"
|
||||
f" the index implies {expected}"
|
||||
)
|
||||
(directory / f"{index:05d}.zst").write_bytes(frame)
|
||||
(directory / f"{index:05d}.raw").write_bytes(plain)
|
||||
count += 1
|
||||
result = subprocess.run(
|
||||
["dart", "run", "tool/verify.dart", str(directory)],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
line = result.stdout.strip().splitlines()[-1] if result.stdout else ""
|
||||
print(f"{path.name}: {count} blocks, {line or result.stderr.strip()}")
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("paths", nargs="+", type=Path)
|
||||
parser.add_argument(
|
||||
"--every",
|
||||
type=int,
|
||||
default=1,
|
||||
help="when a directory is named, take every Nth container",
|
||||
)
|
||||
parser.add_argument("--limit", type=int, default=0)
|
||||
arguments = parser.parse_args()
|
||||
|
||||
containers: list[Path] = []
|
||||
for path in arguments.paths:
|
||||
if path.is_dir():
|
||||
found = sorted(path.glob("*.hgtz"))
|
||||
containers += found[:: arguments.every]
|
||||
else:
|
||||
containers.append(path)
|
||||
if arguments.limit:
|
||||
containers = containers[: arguments.limit]
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
failures = [c for c in containers if not check(c, root)]
|
||||
if failures:
|
||||
print(f"FAILED on {len(failures)} of {len(containers)} containers")
|
||||
return 1
|
||||
print(f"all {len(containers)} containers match pyzstd byte for byte")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
52
tool/verify.dart
Normal file
52
tool/verify.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pure_zstd/pure_zstd.dart';
|
||||
|
||||
/// Decodes every `*.zst` in a directory and compares it to the `*.raw`
|
||||
/// beside it. Driven by tool/oracle.py, which writes the pair.
|
||||
void main(List<String> arguments) {
|
||||
if (arguments.length != 1) {
|
||||
stderr.writeln('usage: verify.dart <directory>');
|
||||
exit(2);
|
||||
}
|
||||
final directory = Directory(arguments.single);
|
||||
final frames =
|
||||
directory
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((file) => file.path.endsWith('.zst'))
|
||||
.toList()
|
||||
..sort((a, b) => a.path.compareTo(b.path));
|
||||
|
||||
final decoder = ZstdDecoder();
|
||||
var checked = 0;
|
||||
var bytes = 0;
|
||||
for (final frame in frames) {
|
||||
final expected = File(
|
||||
frame.path.replaceRange(frame.path.length - 4, null, '.raw'),
|
||||
).readAsBytesSync();
|
||||
final Uint8List actual;
|
||||
try {
|
||||
actual = decoder.decode(frame.readAsBytesSync());
|
||||
} on ZstdException catch (error) {
|
||||
stderr.writeln('${frame.path}: $error');
|
||||
exit(1);
|
||||
}
|
||||
if (actual.length != expected.length) {
|
||||
stderr.writeln(
|
||||
'${frame.path}: ${actual.length} bytes, expected ${expected.length}',
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
for (var i = 0; i < actual.length; i++) {
|
||||
if (actual[i] != expected[i]) {
|
||||
stderr.writeln('${frame.path}: byte $i differs');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
checked++;
|
||||
bytes += actual.length;
|
||||
}
|
||||
stdout.writeln('$checked frames, $bytes bytes, identical');
|
||||
}
|
||||
Loading…
Reference in a new issue