Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions lib/src/utils/buffer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,40 +35,63 @@ 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;
}

if (size <= _bufferedBytes - _cursor) {
// 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
Expand Down Expand Up @@ -99,6 +122,7 @@ class Buffer {
_throwOnNoData();
}
}
fileCursor += size;
return result;
}
}
Expand All @@ -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);
}
Expand Down
77 changes: 77 additions & 0 deletions test/utils/buffer_test.dart
Original file line number Diff line number Diff line change
@@ -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<MetadataParserException>()),
);
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<int>.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();
}
});
}
Loading