From 7ccd6ce2376d8a48911724e8d82f86e781df5c4d Mon Sep 17 00:00:00 2001 From: clement Date: Mon, 14 Sep 2026 23:36:33 -0300 Subject: [PATCH] Fix truncated buffer reads --- lib/src/utils/buffer.dart | 49 +++++++++++++++++------ test/utils/buffer_test.dart | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 test/utils/buffer_test.dart diff --git a/lib/src/utils/buffer.dart b/lib/src/utils/buffer.dart index 2c376c8..68bcfbd 100644 --- a/lib/src/utils/buffer.dart +++ b/lib/src/utils/buffer.dart @@ -35,33 +35,55 @@ class Buffer { _cursor = 0; } - /// Throws a [MetadataParserException] if a previous call to [_fill] - /// was unable to read any data from the file. - /// - /// Once the end of the file is reached, subsequent reads from - /// [RandomAccessFile] will read 0 bytes without failing. This - /// can cause [read] below to infinite loop. + /// Rejects negative byte counts. + void _validateSize(int size) { + if (size < 0) { + throw ArgumentError.value(size, 'size', 'Must not be negative'); + } + } + + /// Throws if [size] bytes are not available. + void _ensureBytesAvailable(int size) { + final int bytesAvailable = remainingBytes; + if (size > bytesAvailable) { + throw MetadataParserException( + track: File(''), + message: 'Expected $size bytes but only $bytesAvailable remain in file', + ); + } + } + + /// Throws when EOF is reached during a read. void _throwOnNoData() { if (_bufferedBytes == 0) { throw MetadataParserException( - track: File(""), message: "Expected more data in file"); + track: File(''), + message: 'Expected more data in file', + ); } } + /// Reads exactly [size] bytes. Uint8List read(int size) { - fileCursor += size; + _validateSize(size); + _ensureBytesAvailable(size); - // if we read something big (~100kb), we can read it directly from file - // it makes the read faster - // no need to use the buffer + // Large payloads bypass the buffer. if (size > _bufferSize) { final result = Uint8List(size); final remaining = _bufferedBytes - _cursor; if (remaining > 0) { result.setRange(0, remaining, _buffer, _cursor); } - randomAccessFile.readIntoSync(result, remaining); + final int bytesRead = randomAccessFile.readIntoSync(result, remaining); + if (bytesRead != size - remaining) { + throw MetadataParserException( + track: File(''), + message: 'File was truncated while reading $size bytes', + ); + } _fill(); + fileCursor += size; return result; } @@ -69,6 +91,7 @@ class Buffer { // Data fits within the current buffer final result = _buffer.sublist(_cursor, _cursor + size); _cursor += size; + fileCursor += size; return result; } else { // Data exceeds remaining buffer, needs refill @@ -99,6 +122,7 @@ class Buffer { _throwOnNoData(); } } + fileCursor += size; return result; } } @@ -108,6 +132,7 @@ class Buffer { /// May return a smaller list if [remainingBytes] is /// less than [size]. Uint8List readAtMost(int size) { + _validateSize(size); final readSize = min(size, remainingBytes); return read(readSize); } diff --git a/test/utils/buffer_test.dart b/test/utils/buffer_test.dart new file mode 100644 index 0000000..6c1fd3b --- /dev/null +++ b/test/utils/buffer_test.dart @@ -0,0 +1,77 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:audio_metadata_reader/audio_metadata_reader.dart'; +import 'package:audio_metadata_reader/src/utils/buffer.dart'; +import 'package:test/test.dart'; + +import '../test_helpers.dart'; + +void main() { + test('read rejects a large exact read from a truncated file', () { + final File file = createTemporaryFile( + 'truncated-binary-data', + Uint8List.fromList([0x01, 0x02, 0x03]), + ); + final RandomAccessFile reader = file.openSync(); + final Buffer buffer = Buffer(randomAccessFile: reader); + + try { + expect( + () => buffer.read(20000), + throwsA(isA()), + ); + expect(buffer.fileCursor, equals(0)); + } finally { + reader.closeSync(); + } + }); + + test('readAtMost returns only the bytes available from a truncated file', () { + final File file = createTemporaryFile( + 'partial-binary-data', + Uint8List.fromList([0x01, 0x02, 0x03]), + ); + final RandomAccessFile reader = file.openSync(); + final Buffer buffer = Buffer(randomAccessFile: reader); + + try { + expect(buffer.readAtMost(20000), orderedEquals([0x01, 0x02, 0x03])); + expect(buffer.fileCursor, equals(3)); + } finally { + reader.closeSync(); + } + }); + + test('read returns a complete large byte range', () { + final Uint8List expected = Uint8List.fromList( + List.generate(20000, (int index) => index % 256), + ); + final File file = createTemporaryFile('large-binary-data', expected); + final RandomAccessFile reader = file.openSync(); + final Buffer buffer = Buffer(randomAccessFile: reader); + + try { + expect(buffer.read(20000), orderedEquals(expected)); + expect(buffer.fileCursor, equals(20000)); + } finally { + reader.closeSync(); + } + }); + + test('read methods reject negative sizes', () { + final File file = createTemporaryFile( + 'binary-data', + Uint8List.fromList([0x01]), + ); + final RandomAccessFile reader = file.openSync(); + final Buffer buffer = Buffer(randomAccessFile: reader); + + try { + expect(() => buffer.read(-1), throwsArgumentError); + expect(() => buffer.readAtMost(-1), throwsArgumentError); + } finally { + reader.closeSync(); + } + }); +}