Add option to disallow trailing data (closes #11)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bda04b..61b3a02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.3.1
+
+- Add `disallowTrailingData` parameter to `TarReader`. When the option is set,
+ `readNext` will ensure that the input stream does not emit further data after
+ the tar archive has been read fully.
+
## 0.3.0
- Remove outdated references in the documentation
diff --git a/lib/src/reader.dart b/lib/src/reader.dart
index 600c1ca..f4b356f 100644
--- a/lib/src/reader.dart
+++ b/lib/src/reader.dart
@@ -59,8 +59,26 @@
/// - an error was emitted through a tar entry's content stream
bool _isDone = false;
+ /// Whether we should ensure that the stream emits no further data after the
+ /// end of the tar file was reached.
+ final bool _checkNoTrailingData;
+
/// Creates a tar reader reading from the raw [tarStream].
///
+ /// The [disallowTrailingData] parameter can be enabled to assert that the
+ /// [tarStream] contains exactly one tar archive before ending.
+ /// When [disallowTrailingData] is disabled (which is the default), the reader
+ /// will automatically cancel its stream subscription when [moveNext] returns
+ /// `false`.
+ /// When it is enabled and a marker indicating the end of an archive is
+ /// encountered, [moveNext] will wait for further events on the stream. If
+ /// further data is received, a [TarException] will be thrown and the
+ /// subscription will be cancelled. Otherwise, [moveNext] effectively waits
+ /// for a done event, making a cancellation unecessary.
+ /// Depending on the input stream, cancellations may cause unintended
+ /// side-effects. In that case, [disallowTrailingData] can be used to ensure
+ /// that the stream is only cancelled if it emits an invalid tar file.
+ ///
/// The [maxSpecialFileSize] parameter can be used to limit the maximum length
/// of hidden entries in the tar stream. These entries include extended PAX
/// headers or long names in GNU tar. The content of those entries has to be
@@ -68,8 +86,10 @@
/// avoid memory-based denial-of-service attacks, this library limits their
/// maximum length. Changing the default of 2 KiB is rarely necessary.
TarReader(Stream<List<int>> tarStream,
- {int maxSpecialFileSize = defaultSpecialLength})
+ {int maxSpecialFileSize = defaultSpecialLength,
+ bool disallowTrailingData = false})
: _chunkedStream = ChunkedStreamIterator(tarStream),
+ _checkNoTrailingData = disallowTrailingData,
_maxSpecialFileSize = maxSpecialFileSize;
@override
@@ -125,11 +145,12 @@
HeaderImpl? nextHeader;
- /// Externally, [next] iterates through the tar archive as if it is a series
- /// of files. Internally, the tar format often uses fake "files" to add meta
- /// data that describes the next file. These meta data "files" should not
- /// normally be visible to the outside. As such, this loop iterates through
- /// one or more "header files" until it finds a "normal file".
+ // Externally, [moveNext] iterates through the tar archive as if it is a
+ // series of files. Internally, the tar format often uses fake "files" to
+ // add meta data that describes the next file. These meta data "files"
+ // should not normally be visible to the outside. As such, this loop
+ // iterates through one or more "header files" until it finds a
+ // "normal file".
while (true) {
if (_skipNext > 0) {
await _readFullBlock(_skipNext);
@@ -142,7 +163,7 @@
nextHeader = await _readHeader(rawHeader);
if (nextHeader == null) {
if (eofAcceptable) {
- await cancel();
+ await _handleExpectedEof();
return false;
} else {
_unexpectedEof();
@@ -224,6 +245,8 @@
_listenedToContentsOnce = false;
_isReadingHeaders = false;
+ // Note: Calling cancel is safe when the stream has already been completed.
+ // It's a noop in that case, which is what we want.
return _chunkedStream.cancel();
}
@@ -286,6 +309,20 @@
return size;
}
+ Future<void> _handleExpectedEof() async {
+ if (_checkNoTrailingData) {
+ // This will be empty iff the stream is done
+ final furtherData = await _chunkedStream.read(1);
+
+ // Note that throwing will automatically cancel the reader
+ if (furtherData.isNotEmpty) {
+ throw TarException('Illegal content after the end of the tar archive.');
+ }
+ }
+
+ await cancel();
+ }
+
Never _unexpectedEof() {
throw TarException.header('Unexpected end of file');
}
diff --git a/test/reader_test.dart b/test/reader_test.dart
index 3a9a3d8..a1e2ba4 100644
--- a/test/reader_test.dart
+++ b/test/reader_test.dart
@@ -71,7 +71,7 @@
final zeroBlock = Uint8List(512);
final controller = StreamController<List<int>>();
controller.onListen = () {
- controller..add(zeroBlock)..add(zeroBlock);
+ controller..add(zeroBlock)..add(zeroBlock)..add(zeroBlock);
};
final reader = TarReader(controller.stream);
@@ -114,6 +114,54 @@
});
});
+ group('disallowTrailingData: true', () {
+ final emptyBlock = Uint8List(512);
+
+ void closeLater(StreamController<Object?> controller) {
+ controller.onCancel = () => fail('Should not cancel stream subscription');
+
+ Timer.run(() {
+ // Calling close() implicitly cancels the stream subscription, we only
+ // want to ensure that the reader is not doing that.
+ controller.onCancel = null;
+ expect(controller.hasListener, isTrue,
+ reason: 'Should have a listener');
+
+ controller.close();
+ });
+ }
+
+ test('throws for trailing data', () async {
+ final input = StreamController<Uint8List>()
+ ..add(emptyBlock)
+ ..add(emptyBlock)
+ ..add(Uint8List(1)); // illegal content after end marker
+
+ final reader = TarReader(input.stream, disallowTrailingData: true);
+ await expectLater(reader.moveNext(), throwsA(isA<TarException>()));
+ expect(input.hasListener, isFalse,
+ reason: 'Should have cancelled subscription after error.');
+ });
+
+ test('does not throw or cancel if the stream ends as expected', () {
+ final input = StreamController<Uint8List>()
+ ..add(emptyBlock)
+ ..add(emptyBlock);
+ closeLater(input);
+
+ final reader = TarReader(input.stream, disallowTrailingData: true);
+ expectLater(reader.moveNext(), completion(isFalse));
+ });
+
+ test('does not throw or cancel if the stream ends without marker', () {
+ final input = StreamController<Uint8List>();
+ closeLater(input);
+
+ final reader = TarReader(input.stream, disallowTrailingData: true);
+ expectLater(reader.moveNext(), completion(isFalse));
+ });
+ });
+
group('tests from dart-neats PR', () {
Stream<List<int>> open(String name) {
return File('reference/neats_test/$name').openRead();