Document the public surface

This commit is contained in:
Tag 2026-09-22 19:48:12 -06:00
commit d7fcebe694
3 changed files with 21 additions and 32 deletions

View file

@ -1,7 +1,7 @@
/// A pure Dart zstd decompressor. /// A pure Dart zstd decompressor.
/// ///
/// Decompression only, and only the subset of RFC 8878 that the elevation /// Decompression only, one RFC 8878 frame per call, on native platforms. It
/// containers this was written for use. See the README for what is left out. /// refuses dictionaries and skippable frames; see the README for the rest.
library; library;
export 'src/decoder.dart' show ZstdDecoder, zstdDecode; export 'src/decoder.dart' show ZstdDecoder, zstdDecode;

View file

@ -13,33 +13,17 @@ import 'xxhash.dart';
/// One instance is not safe to share between isolates, and a frame must be /// One instance is not safe to share between isolates, and a frame must be
/// decoded to completion before the next one starts. /// decoded to completion before the next one starts.
class ZstdDecoder { class ZstdDecoder {
/// Creates a decoder that refuses any frame larger than [maxOutputSize].
ZstdDecoder({this.maxOutputSize = defaultMaxOutputSize}); ZstdDecoder({this.maxOutputSize = defaultMaxOutputSize});
/// The four bytes every zstd frame starts with, read little-endian.
static const int magic = 0xFD2FB528; static const int magic = 0xFD2FB528;
/// The largest output this decoder will allocate for one frame. /// The largest output a decoder allocates for one frame unless told
/// otherwise, since a frame header can claim up to 2^64 bytes.
/// ///
/// `Frame_Content_Size` is up to a 64-bit number taken straight from the /// The top-level [zstdDecode] always uses it; raising it takes a
/// input and it used to size the output buffer unchecked, so a sixteen-byte /// [ZstdDecoder] of one's own.
/// frame could ask for four exbibytes. It is reachable by accident as well
/// as by design: one bit flipped in a real tile's frame descriptor moves
/// the content-size flag from 2 to 3, eight bytes of compressed data are
/// read as the size, and the eighth lands on the sign bit. That is a
/// `RangeError` for a negative length, or an `OutOfMemoryError` for a large
/// positive one, and both are `Error` rather than `Exception`, so a caller
/// writing `on ZstdException` does not catch either and the isolate dies.
///
/// RFC 8878 section 3.1.1.1.2 lets a decoder refuse a frame that asks for
/// more memory than it is willing to give, which is the job of the window
/// descriptor this package skips. Allocating the output whole is why it can
/// skip it, and this is the bound that replaces it.
///
/// A caller that knows the exact size should pass [decode]'s `expectedSize`
/// as well, and gets an exact check *in addition to* this ceiling rather
/// than instead of it: a frame declaring 70 MiB is refused here before the
/// two are ever compared. Raising it takes a `ZstdDecoder` of one's own,
/// since the top-level [zstdDecode] does not offer it. The `.hgtz` reader
/// knows its size: a block is `height * width * 2`.
static const int defaultMaxOutputSize = 64 * 1024 * 1024; static const int defaultMaxOutputSize = 64 * 1024 * 1024;
/// The ceiling this instance enforces. See [defaultMaxOutputSize]. /// The ceiling this instance enforces. See [defaultMaxOutputSize].
@ -74,10 +58,12 @@ class ZstdDecoder {
int _literalAt = 0; int _literalAt = 0;
int _literalEnd = 0; int _literalEnd = 0;
/// Decodes one frame. /// Decodes exactly one frame, which must be the whole of [src], into a new
/// list.
/// ///
/// [expectedSize] is only needed when the frame header omits its content /// [expectedSize] is required when the frame omits its content size, and
/// size, which the containers this was written for never do. /// otherwise must match it exactly. Throws a [ZstdException] for any frame
/// that is malformed, too large, fails its checksum, or uses a dictionary.
Uint8List decode(Uint8List src, {int? expectedSize}) { Uint8List decode(Uint8List src, {int? expectedSize}) {
if (src.length < 6) { if (src.length < 6) {
throw const ZstdException('too short to be a zstd frame'); throw const ZstdException('too short to be a zstd frame');
@ -151,9 +137,7 @@ class ZstdDecoder {
_repeat2 = 4; _repeat2 = 4;
_repeat3 = 8; _repeat3 = 8;
// Only when the frame carries one. Hashing every frame would cost the // Only when the frame carries one, so a frame without pays nothing.
// .hgtz reader throughput it has no use for, since pyzstd writes no
// checksum and the container's own SHA-256 is verified on download.
final hash = checksum == 1 ? (_hash..reset()) : null; final hash = checksum == 1 ? (_hash..reset()) : null;
var written = 0; var written = 0;
@ -674,7 +658,7 @@ class ZstdDecoder {
/// Decodes one zstd frame into a new list. /// Decodes one zstd frame into a new list.
/// ///
/// Allocates a decoder per call. Decoding many frames, which is what reading /// Allocates a decoder per call, so decoding many frames should hold a
/// a tile does, should hold a [ZstdDecoder] instead. /// [ZstdDecoder] instead. Throws as [ZstdDecoder.decode] does.
Uint8List zstdDecode(Uint8List src, {int? expectedSize}) => Uint8List zstdDecode(Uint8List src, {int? expectedSize}) =>
ZstdDecoder().decode(src, expectedSize: expectedSize); ZstdDecoder().decode(src, expectedSize: expectedSize);

View file

@ -1,7 +1,12 @@
/// Thrown when a frame is malformed, or uses a feature this decoder omits. /// Thrown when a frame is malformed, or uses a feature this decoder omits.
///
/// Every refusal from this package is one of these, so `on ZstdException` is
/// enough to survive untrusted input.
class ZstdException implements Exception { class ZstdException implements Exception {
/// Creates an exception carrying [message].
const ZstdException(this.message); const ZstdException(this.message);
/// What was wrong with the frame, for a log rather than for matching on.
final String message; final String message;
@override @override