diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8eb0f75..6de334f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+## 0.9.3
+- Support decoding N-dimensional arrays, which are a draft 3 construct that was previously
+ rejected as invalid
+ - `[$type#[Nx Ny ...]` decodes to nested lists, with the innermost axis kept as the typed
+ list. The nested lists are views onto one buffer rather than copies, so the payload is
+ still a single contiguous allocation
+ - The column-major form, `[$type#[[Nx Ny ...]]` as written by MATLAB and FORTRAN, is
+ reordered into row-major order so that it reads the same way
+ - Dimension arrays are accepted in both optimized and non-optimized form
+ - Encoding N-dimensional arrays is not supported, so a decoded array is written back as
+ nested arrays; the values are unchanged
+- Throw a `FormatException` instead of a `RangeError` when a string or buffer runs past the
+ end of the input
+
## 0.9.2
- Update description to improve package score
diff --git a/README.md b/README.md
index 7022962..74e08d1 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,10 @@ echo -n "[1, 2, 3]" | bjdata print
## Types
### Decoding BJData to Dart
-- Multi dimensional arrays are not yet supported.
+- N-dimensional arrays (`#[Nx Ny ...]`) decode to nested lists, with the innermost axis
+ kept as the typed list. Both row-major and column-major (`#[[Nx Ny ...]]`) orderings are
+ read; a column-major payload is reordered so that it reads the same way. Encoding
+ N-dimensional arrays is not yet supported, so they are written back as nested arrays.
| BJData Type | Marker | Dart |
|------------------|--------|--------------------------------|
@@ -100,6 +103,7 @@ echo -n "[1, 2, 3]" | bjdata print
| `array[float32]` | `[$d` | `Float32List` |
| `array[float64]` | `[$D` | `Float64List` |
| `object` | `{}` | `Map` |
+| `array[T]` N-D | `#[` | Nested `List` of `T` |
\*
Warning: `int` in Dart is a signed 64-bit integer. `uint64`/`M` values are decoded as `int64`
diff --git a/lib/src/decoder/sink.dart b/lib/src/decoder/sink.dart
index da0b9e9..3915639 100644
--- a/lib/src/decoder/sink.dart
+++ b/lib/src/decoder/sink.dart
@@ -64,6 +64,9 @@ class BjdataReader {
}
Uint8List _readUint8ListView(int length) {
+ if (_offset + length > _bytes.lengthInBytes) {
+ throw FormatException("Unexpected end of input", _bytes, _bytes.lengthInBytes);
+ }
final view = Uint8List.sublistView(_bytes, _offset, _offset + length);
_offset += length;
return view;
@@ -187,20 +190,21 @@ class BjdataReader {
};
}
+ int _readIntForMarker(BjdataMarker marker, String what, int offsetBefore) => switch (marker) {
+ BjdataMarker.uint8 => _readUint8(),
+ BjdataMarker.int8 => _readInt8(),
+ BjdataMarker.uint16 => _readUint16(),
+ BjdataMarker.int16 => _readInt16(),
+ BjdataMarker.uint32 => _readUint32(),
+ BjdataMarker.int32 => _readInt32(),
+ BjdataMarker.uint64 => _readUint64(),
+ BjdataMarker.int64 => _readInt64(),
+ _ => throw FormatException('Unexpected non-$what type marker: $marker', _bytes, offsetBefore),
+ };
+
int _readLength() {
final offsetBefore = _offset;
- final marker = _readMarker();
- final length = switch (marker) {
- BjdataMarker.uint8 => _readUint8(),
- BjdataMarker.int8 => _readInt8(),
- BjdataMarker.uint16 => _readUint16(),
- BjdataMarker.int16 => _readInt16(),
- BjdataMarker.uint32 => _readUint32(),
- BjdataMarker.int32 => _readInt32(),
- BjdataMarker.uint64 => _readUint64(),
- BjdataMarker.int64 => _readInt64(),
- _ => throw FormatException('Unexpected non-length type marker: $marker', _bytes, offsetBefore),
- };
+ final length = _readIntForMarker(_readMarker(), 'length', offsetBefore);
if (length < 0) throw FormatException('Negative length: $length', _bytes, offsetBefore);
return length;
}
@@ -279,10 +283,68 @@ class BjdataReader {
BigInt _readHuge() => BigInt.parse(_readString());
String _readString() => utf8.decode(_readUint8ListView(_readLength()));
- (BjdataMarker?, int?) _readStrongTypeAndCount() {
+ /// The element count of a container, and the dimensions it was given as.
+ ///
+ /// A count is either a single integer, a dimension array (`#[Nx Ny ...]`) for
+ /// an N-dimensional array serialized in row-major order, or a dimension array
+ /// wrapped in a single element array (`#[[Nx Ny ...]]`) for one serialized in
+ /// column-major order, as MATLAB and FORTRAN write it.
+ ({int count, List? dimensions, bool columnMajor}) _readCount() {
+ final offsetBefore = _offset;
+ if (!_peekMarkerConsumeIf(BjdataMarker.arrayOpen)) {
+ return (count: _readLength(), dimensions: null, columnMajor: false);
+ }
+
+ // A second '[' wraps the dimension array, which marks column-major order.
+ final columnMajor = _peekMarker() == BjdataMarker.arrayOpen;
+ if (columnMajor) _offset++;
+
+ final dimensions = _readDimensions(offsetBefore);
+
+ if (columnMajor && !_peekMarkerConsumeIf(BjdataMarker.arrayClose)) {
+ throw FormatException('Expected end of the wrapped dimension array', _bytes, _offset);
+ }
+ return (
+ count: dimensions.fold(1, (a, b) => a * b),
+ dimensions: dimensions,
+ columnMajor: columnMajor,
+ );
+ }
+
+ /// Reads a dimension array, with its leading `[` already consumed.
+ List _readDimensions(int offsetBefore) {
BjdataMarker? strongType;
int? count;
+ if (_peekMarkerConsumeIf(BjdataMarker.strongType)) {
+ strongType = _readMarker();
+ if (!strongType.isIntegerType) {
+ throw FormatException('Dimensions must be of an integer type: $strongType', _bytes, _offset - 1);
+ }
+ if (!_peekMarkerConsumeIf(BjdataMarker.count)) {
+ throw FormatException('Expected count marker to follow strong type', _bytes, _offset);
+ }
+ count = _readLength();
+ } else if (_peekMarkerConsumeIf(BjdataMarker.count)) {
+ count = _readLength();
+ }
+
+ final dimensions = [];
+ for (var i = 0; count != null ? i < count : true; i++) {
+ if (count == null && _peekMarkerConsumeIf(BjdataMarker.arrayClose)) break;
+ final before = _offset;
+ final dimension = _readIntForMarker(strongType ?? _readMarker(), 'dimension', before);
+ if (dimension < 0) throw FormatException('Negative dimension: $dimension', _bytes, before);
+ dimensions.add(dimension);
+ }
+ if (dimensions.isEmpty) throw FormatException('Empty dimension array', _bytes, offsetBefore);
+ return dimensions;
+ }
+
+ ({BjdataMarker? strongType, int? count, List? dimensions, bool columnMajor}) _readStrongTypeAndCount() {
+ BjdataMarker? strongType;
+ const int? count = null;
+
if (_peekMarkerConsumeIf(BjdataMarker.strongType)) {
// Read strong type
strongType = _readMarker();
@@ -302,21 +364,23 @@ class BjdataReader {
}
// Read count
- count = _readLength();
+ final (:count, :dimensions, :columnMajor) = _readCount();
+ return (strongType: strongType, count: count, dimensions: dimensions, columnMajor: columnMajor);
} else if (_peekMarkerConsumeIf(BjdataMarker.count)) {
// Read count
- count = _readLength();
+ final (:count, :dimensions, :columnMajor) = _readCount();
+ return (strongType: strongType, count: count, dimensions: dimensions, columnMajor: columnMajor);
}
- return (strongType, count);
+ return (strongType: strongType, count: count, dimensions: null, columnMajor: false);
}
Object _readArray() {
final offsetBefore = _offset - 1;
- final (strongType, count) = _readStrongTypeAndCount();
+ final (:strongType, :count, :dimensions, :columnMajor) = _readStrongTypeAndCount();
if (strongType != null && strongType.isValidStrongType && strongType != BjdataMarker.char) {
- return switch (strongType) {
+ final flat = switch (strongType) {
BjdataMarker.byte => _readByteDataCopy(count!),
BjdataMarker.uint8 => _readUint8ListCopy(count!),
BjdataMarker.int8 => _readInt8ListCopy(count!),
@@ -331,6 +395,8 @@ class BjdataReader {
BjdataMarker.float64 => _readFloat64ListCopy(count!),
_ => throw FormatException('Invalid strong type: $strongType', _bytes, offsetBefore),
};
+ if (dimensions == null) return flat;
+ return _reshape(columnMajor ? _toRowMajor(flat, dimensions) : flat, dimensions, 0);
}
final list =