decodeAttributeStreamBody function
FourdgsAttributeStream
decodeAttributeStreamBody(
- FourdgsCursor cursor,
- FourdgsStreamHeader header, {
- int streamOffset = 0,
- int? chunkOffset,
Decodes the payload of a stream whose header has already been read.
Implementation
FourdgsAttributeStream decodeAttributeStreamBody(
FourdgsCursor cursor,
FourdgsStreamHeader header, {
int streamOffset = 0,
int? chunkOffset,
}) {
final attributeId = header.attributeId;
final width = header.width;
final mode = header.mode;
final codec = header.codec;
final channels = header.channels;
final count = header.count;
final payloadLength = header.payloadLength;
if (count == 0) {
cursor.skip(payloadLength);
return FourdgsAttributeStream(
attributeId: attributeId,
channels: channels,
count: 0,
values: Int32List(0),
);
}
if (width != 1 && width != 2 && width != 4) {
throw FourdgsMalformedFile(
'attribute $attributeId: bad symbol width $width',
);
}
if (channels == 0) {
throw FourdgsMalformedFile('attribute $attributeId: zero channels');
}
final symbols = mode == modeConst ? channels : count * channels;
final expected = symbols * width;
if (expected > maxStreamBytes) {
throw FourdgsMalformedFile(
'attribute $attributeId declares $expected bytes, past the $maxStreamBytes cap',
);
}
// The cap above bounds what is *decompressed*; this bounds what is
// *materialized*, and they are not the same number. A constant-mode stream
// stores `channels` symbols and repeats them `element_count` times, so a
// handful of payload bytes can name a four-billion-element array — the cheap
// half of a decompression bomb, and one the size ceiling alone does not see.
if (count * channels > maxStreamBytes ~/ 4) {
throw FourdgsMalformedFile(
'attribute $attributeId would expand to ${count * channels} values, past the cap',
);
}
final context =
'attribute $attributeId stream header at byte $streamOffset'
'${chunkOffset == null ? '' : ' in the Chunk at byte $chunkOffset'}';
final raw = _decompress(cursor.take(payloadLength), codec, expected, context);
final sym = _unshuffle(raw, width, symbols);
final values = Int32List(count * channels);
if (mode == modeConst) {
// Exactly `channels` symbols, repeated `count` times.
for (int c = 0; c < channels; c++) {
final v = _unzigzag(sym[c]);
for (int i = 0; i < count; i++) {
values[i * channels + c] = v;
}
}
} else {
for (int i = 0; i < symbols; i++) {
values[i] = _unzigzag(sym[i]);
}
if (mode == modeDelta) {
// Delta runs along element order, so a channel accumulates against the
// same channel of the previous element.
//
// The running sum is checked rather than allowed to wrap. Reading an
// Int32List element yields a 64-bit Dart int, so the addition below
// happens before any truncation and the overflow is visible here; storing
// it would silently turn a malformed stream into a plausible wrong
// number. That matters most for `object_id` (section 6.6), where the code
// is a label rather than a bin: a wrapped code is a *different object*,
// and the Rust reader — which accumulates in i64 and then requires the
// code to fit an i32 — refuses the same file.
for (int i = channels; i < values.length; i++) {
final acc = values[i] + values[i - channels];
if (acc > 2147483647 || acc < -2147483648) {
throw FourdgsMalformedFile(
'attribute $attributeId: delta stream leaves the 32-bit range at '
'element ${i ~/ channels}',
);
}
values[i] = acc;
}
} else if (mode != modeRaw) {
throw FourdgsMalformedFile(
'attribute $attributeId: unknown stream mode $mode',
);
}
}
return FourdgsAttributeStream(
attributeId: attributeId,
channels: channels,
count: count,
values: values,
);
}