Streaming encoders leave Frame_Content_Size out unless told the size up front, and their readers often can't know it either: every zstd frame in an Anki package (its collection, its media list, each media file) is written that way. Such a frame needed the exact size passed as expectedSize, and anything else was refused. Now it gets the most its blocks can hold, Block_Maximum_Size (128 KiB) each, found by walking the block headers alone, and the output is trimmed to what the blocks held. The output is still allocated once. expectedSize keeps its exact check when given, and maxOutputSize bounds the allocation as before. Checked against libzstd (python-zstandard) on Anki 26.09.3's exports: a small package's four frames and a 30 MB collection decode byte for byte the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
5.3 KiB
Dart
151 lines
5.3 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:pure_zstd/pure_zstd.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import 'corpus.dart';
|
|
|
|
/// The committed corpus, which exists because `hgtz_block_test.dart` decodes
|
|
/// one container from one producer at one level and so proves the sequence
|
|
/// loop and Huffman literals on level-19 terrain and nothing else.
|
|
///
|
|
/// The frames here reach the parts of RFC 8878 that 16,490 real `.hgtz`
|
|
/// blocks never touch: raw blocks, RLE blocks, RLE literals, a frame with no
|
|
/// content size, a frame with a checksum, the eight-byte content size, and
|
|
/// the direct-nibble Huffman weight form.
|
|
///
|
|
/// Nothing below asserts what this decoder happens to do. A legal frame is
|
|
/// compared against what pyzstd decompressed it to; a refused frame is one
|
|
/// pyzstd also refuses. `tool/make_corpus.py` records both, along with the
|
|
/// exact command behind each frame, in `test/fixtures/corpus/manifest.json`.
|
|
void main() {
|
|
final corpus = loadCorpus();
|
|
|
|
group('a legal frame decodes to the bytes pyzstd gives', () {
|
|
for (final entry in corpus.where(
|
|
(entry) => entry.legal && entry.trailing == null,
|
|
)) {
|
|
test('${entry.name}: ${entry.command}', () {
|
|
final out = ZstdDecoder().decode(
|
|
entry.frame,
|
|
expectedSize: entry.expectedSize,
|
|
);
|
|
expect(out.length, entry.size);
|
|
expect(out, orderedEquals(entry.plain));
|
|
});
|
|
}
|
|
});
|
|
|
|
group('a malformed frame is refused, never crashed on', () {
|
|
for (final entry in corpus.where((entry) => !entry.legal)) {
|
|
// A `ZstdException` and nothing else. `RangeError`, `OutOfMemoryError`
|
|
// and `IndexError` are all `Error` rather than `Exception`, so a caller
|
|
// catching `ZstdException` does not catch them and the isolate dies.
|
|
test('${entry.name}: ${entry.why}', () {
|
|
expect(
|
|
() => ZstdDecoder().decode(entry.frame),
|
|
throwsA(isA<ZstdException>()),
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
group('two frames in one buffer', () {
|
|
final entry = corpus.singleWhere((entry) => entry.name == 'twoframes');
|
|
final first = 5000;
|
|
|
|
test('${entry.command}: decodes to both or refuses, never to the first '
|
|
'alone', () {
|
|
Uint8List? out;
|
|
try {
|
|
out = ZstdDecoder().decode(entry.frame);
|
|
} on ZstdException {
|
|
return; // Refusing is the other acceptable answer.
|
|
}
|
|
expect(
|
|
out.length,
|
|
isNot(first),
|
|
reason: 'returned the first frame and stopped, raising nothing',
|
|
);
|
|
expect(out, orderedEquals(entry.plain));
|
|
});
|
|
});
|
|
|
|
group('a frame whose declared size is not the one asked for', () {
|
|
// A guard callers rest on, and it had no test:
|
|
// deleting the whole check left every other case green. A caller that
|
|
// knows the size, as the .hgtz reader does, gets an exact comparison
|
|
// against the frame's own figure rather than only the ceiling. Left to
|
|
// the end, a frame declaring less decoded its own content happily and a
|
|
// frame declaring more allocated whatever it asked for first.
|
|
for (final entry in corpus.where(
|
|
// Not a buffer holding more than one frame: this decoder refuses
|
|
// those whatever size it is told to expect.
|
|
(entry) =>
|
|
entry.legal &&
|
|
entry.expectedSize == null &&
|
|
entry.trailing == null &&
|
|
entry.size! > 1,
|
|
)) {
|
|
test('${entry.name} is refused when asked for one byte more', () {
|
|
expect(
|
|
() =>
|
|
ZstdDecoder().decode(entry.frame, expectedSize: entry.size! + 1),
|
|
throwsA(isA<ZstdException>()),
|
|
);
|
|
});
|
|
|
|
test('${entry.name} still decodes when asked for the size it has', () {
|
|
expect(
|
|
ZstdDecoder().decode(entry.frame, expectedSize: entry.size),
|
|
orderedEquals(entry.plain),
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
group('a frame declaring no content size', () {
|
|
final entry = corpus.singleWhere((entry) => entry.name == 'nofcs_windowed');
|
|
final size = entry.expectedSize!;
|
|
|
|
test('decodes when given the size it really has', () {
|
|
final out = ZstdDecoder().decode(entry.frame, expectedSize: size);
|
|
expect(out, orderedEquals(entry.plain));
|
|
});
|
|
|
|
for (final wrong in <int>[size - 1, size + 1, 0]) {
|
|
test('is refused when given $wrong instead of $size', () {
|
|
expect(
|
|
() => ZstdDecoder().decode(entry.frame, expectedSize: wrong),
|
|
throwsA(isA<ZstdException>()),
|
|
);
|
|
});
|
|
}
|
|
|
|
// Streaming encoders write frames like this one, and their callers often
|
|
// can't know the size: an Anki package's collection, for one.
|
|
test('decodes without being told its size, to exactly what it holds', () {
|
|
final out = ZstdDecoder().decode(entry.frame);
|
|
expect(out.length, size);
|
|
expect(out, orderedEquals(entry.plain));
|
|
});
|
|
|
|
test('is refused, not crashed on, when cut short', () {
|
|
for (final cut in [1, 3, entry.frame.length ~/ 2]) {
|
|
expect(
|
|
() => ZstdDecoder().decode(
|
|
Uint8List.sublistView(entry.frame, 0, entry.frame.length - cut),
|
|
),
|
|
throwsA(isA<ZstdException>()),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('is refused when its blocks could hold more than allowed', () {
|
|
expect(
|
|
() => ZstdDecoder(maxOutputSize: size).decode(entry.frame),
|
|
throwsA(isA<ZstdException>()),
|
|
);
|
|
});
|
|
});
|
|
}
|