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.
///
/// Decompression only, and only the subset of RFC 8878 that the elevation
/// containers this was written for use. See the README for what is left out.
/// Decompression only, one RFC 8878 frame per call, on native platforms. It
/// refuses dictionaries and skippable frames; see the README for the rest.
library;
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
/// decoded to completion before the next one starts.
class ZstdDecoder {
/// Creates a decoder that refuses any frame larger than [maxOutputSize].
ZstdDecoder({this.maxOutputSize = defaultMaxOutputSize});
/// The four bytes every zstd frame starts with, read little-endian.
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
/// input and it used to size the output buffer unchecked, so a sixteen-byte
/// 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`.
/// The top-level [zstdDecode] always uses it; raising it takes a
/// [ZstdDecoder] of one's own.
static const int defaultMaxOutputSize = 64 * 1024 * 1024;
/// The ceiling this instance enforces. See [defaultMaxOutputSize].
@ -74,10 +58,12 @@ class ZstdDecoder {
int _literalAt = 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
/// size, which the containers this was written for never do.
/// [expectedSize] is required when the frame omits its content size, and
/// 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}) {
if (src.length < 6) {
throw const ZstdException('too short to be a zstd frame');
@ -151,9 +137,7 @@ class ZstdDecoder {
_repeat2 = 4;
_repeat3 = 8;
// Only when the frame carries one. Hashing every frame would cost the
// .hgtz reader throughput it has no use for, since pyzstd writes no
// checksum and the container's own SHA-256 is verified on download.
// Only when the frame carries one, so a frame without pays nothing.
final hash = checksum == 1 ? (_hash..reset()) : null;
var written = 0;
@ -674,7 +658,7 @@ class ZstdDecoder {
/// Decodes one zstd frame into a new list.
///
/// Allocates a decoder per call. Decoding many frames, which is what reading
/// a tile does, should hold a [ZstdDecoder] instead.
/// Allocates a decoder per call, so decoding many frames should hold a
/// [ZstdDecoder] instead. Throws as [ZstdDecoder.decode] does.
Uint8List zstdDecode(Uint8List src, {int? 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.
///
/// Every refusal from this package is one of these, so `on ZstdException` is
/// enough to survive untrusted input.
class ZstdException implements Exception {
/// Creates an exception carrying [message].
const ZstdException(this.message);
/// What was wrong with the frame, for a log rather than for matching on.
final String message;
@override