Update to Dart 3, maintenance (#27)
* Update to Dart 3
* Run CI weekly
* Bump version in pubspec
* Might as well go stable
* Add doc comments
* Enforce docs for exported members
diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml
index fbdbf21..c76e653 100644
--- a/.github/workflows/dart.yml
+++ b/.github/workflows/dart.yml
@@ -4,7 +4,9 @@
push:
branches: [ main ]
pull_request:
- branches: [ main ]
+ schedule:
+ # Make sure everything is still working by running the CI weekly.
+ - cron: "0 5 * * 1"
jobs:
analyze:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a804552..50b4c7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.0.0
+
+- __Breaking__ Add class modifiers where applicable.
+
## 0.5.6
- Allow cancelling a `TarEntry.contents` subscription before reading more files.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 8b18047..1dd563f 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,9 +1,6 @@
-include: package:extra_pedantic/analysis_options.2.0.0.yaml
+include: package:extra_pedantic/analysis_options.4.0.0.yaml
analyzer:
- strong-mode:
- implicit-casts: false
- implicit-dynamic: false
language:
strict-inference: true
strict-raw-types: true
@@ -12,6 +9,7 @@
rules:
close_sinks: false # This rule has just too many false-positives...
comment_references: true
+ package_api_docs: true
literal_only_boolean_expressions: false # Nothing wrong with a little while(true)
parameter_assignments: false
unnecessary_await_in_return: false
diff --git a/lib/src/entry.dart b/lib/src/entry.dart
index 71d1730..7645bbb 100644
--- a/lib/src/entry.dart
+++ b/lib/src/entry.dart
@@ -1,7 +1,5 @@
import 'dart:async';
-import 'package:meta/meta.dart';
-
import 'header.dart';
/// An entry in a tar file.
@@ -9,8 +7,7 @@
/// Usually, tar entries are read from a stream, and they're bound to the stream
/// from which they've been read. This means that they can only be read once,
/// and that only one [TarEntry] is active at a time.
-@sealed
-class TarEntry {
+final class TarEntry {
/// The parsed [TarHeader] of this tar entry.
final TarHeader header;
@@ -58,7 +55,7 @@
}
/// A tar entry stored in memory.
-class SynchronousTarEntry extends TarEntry {
+final class SynchronousTarEntry extends TarEntry {
/// The contents of this tar entry as a byte array.
final List<int> data;
diff --git a/lib/src/exception.dart b/lib/src/exception.dart
index 3d9e614..fa1fc92 100644
--- a/lib/src/exception.dart
+++ b/lib/src/exception.dart
@@ -1,8 +1,14 @@
import 'package:meta/meta.dart';
/// An exception indicating that there was an issue parsing a `.tar` file.
-/// Intended to be seen by the user.
-class TarException extends FormatException {
+///
+/// The [message] contains reported from this exception contains details on the
+/// location of the parsing error.
+///
+/// This is the only exception that should be thrown by the `tar` package. Other
+/// exceptions are either a bug in this package or errors thrown as a response
+/// to API misuse.
+final class TarException extends FormatException {
@internal
TarException(String message) : super(message);
diff --git a/lib/src/format.dart b/lib/src/format.dart
index ef65ec9..b584f5c 100644
--- a/lib/src/format.dart
+++ b/lib/src/format.dart
@@ -1,49 +1,38 @@
-import 'package:meta/meta.dart';
-
-/// Handy map to help us translate [TarFormat] values to their names.
-/// Be sure to keep this consistent with the constant initializers in
-/// [TarFormat].
-const _formatNames = {
- 1: 'V7',
- 2: 'USTAR',
- 4: 'PAX',
- 8: 'GNU',
- 16: 'STAR',
-};
-
/// Holds the possible TAR formats that a file could take.
///
-/// This library only supports the V7, USTAR, PAX, GNU, and STAR formats.
-@sealed
-class TarFormat {
+/// This library supports the V7, USTAR, PAX, GNU, and STAR formats. The
+/// [MaybeTarFormat] class generally describes any combination of those formats
+/// and represents that we don't know the exact format yet. As soon as we do
+/// know, the [TarFormat] enum represents the exact format of a header.
+sealed class MaybeTarFormat {
/// The TAR formats are encoded in powers of two in [_value], such that we
/// can refine our guess via bit operations as we discover more information
/// about the TAR file.
/// A value of 0 means that the format is invalid.
- final int _value;
+ int get _value;
- const TarFormat._internal(this._value);
-
- @override
- int get hashCode => _value;
-
- @override
- bool operator ==(Object other) {
- if (other is! TarFormat) return false;
-
- return _value == other._value;
+ factory MaybeTarFormat._(int value) {
+ return switch (value) {
+ 1 => TarFormat.v7,
+ 2 => TarFormat.ustar,
+ 4 => TarFormat.pax,
+ 8 => TarFormat.gnu,
+ 16 => TarFormat.star,
+ final other => _MaybeTarFormat(other),
+ };
}
- @override
- String toString() {
- if (!isValid()) return 'Invalid';
-
- final possibleNames = _formatNames.entries
- .where((e) => _value & e.key != 0)
- .map((e) => e.value);
-
- return possibleNames.join(' or ');
- }
+ /// Returns a new [MaybeTarFormat] that signifies that it can be either
+ /// `this` or [other]'s format.
+ ///
+ /// **Example:**
+ /// ```dart
+ /// TarFormat format = TarFormat.ustar | TarFormat.pax;
+ /// ```
+ ///
+ /// The above code would signify that we have limited `format` to either
+ /// the USTAR or PAX format, but need further information to refine the guess.
+ MaybeTarFormat operator |(TarFormat other);
/// Returns if [other] is a possible resolution of `this`.
///
@@ -51,37 +40,7 @@
/// enough information to determine if it is [TarFormat.ustar] or
/// [TarFormat.pax], so either of them could be possible resolutions of
/// `this`.
- bool has(TarFormat other) => _value & other._value != 0;
-
- /// Returns a new [TarFormat] that signifies that it can be either
- /// `this` or [other]'s format.
- ///
- /// **Example:**
- /// ```dart
- /// TarFormat format = TarFormat.USTAR | TarFormat.PAX;
- /// ```
- ///
- /// The above code would signify that we have limited `format` to either
- /// the USTAR or PAX format, but need further information to refine the guess.
- TarFormat operator |(TarFormat other) {
- return mayBe(other);
- }
-
- /// Returns a new [TarFormat] that signifies that it can be either
- /// `this` or [other]'s format.
- ///
- /// **Example:**
- /// ```dart
- /// TarFormat format = TarFormat.PAX;
- /// format = format.mayBe(TarFormat.USTAR);
- /// ```
- ///
- /// The above code would signify that we learnt that in addition to being a
- /// PAX format, it could also be of the USTAR format.
- TarFormat mayBe(TarFormat? other) {
- if (other == null) return this;
- return TarFormat._internal(_value | other._value);
- }
+ bool has(MaybeTarFormat other);
/// Returns a new [TarFormat] that signifies that it can only be [other]'s
/// format.
@@ -98,16 +57,20 @@
///
/// If `has(other) == false`, [mayOnlyBe] will result in an unknown
/// [TarFormat].
- TarFormat mayOnlyBe(TarFormat other) {
- return TarFormat._internal(_value & other._value);
- }
+ MaybeTarFormat mayOnlyBe(MaybeTarFormat other);
/// Returns if this format might be valid.
///
/// This returns true as well even if we have yet to fully determine what the
/// format is.
- bool isValid() => _value > 0;
+ bool get valid;
+}
+/// A fully resolved tar format.
+///
+/// When we know that a tar entry must use a specific format, this is represented
+/// with a value from this [TarFormat] enum.
+enum TarFormat implements MaybeTarFormat {
/// Original Unix Version 7 (V7) AT&T tar tool prior to standardization.
///
/// The structure of the V7 Header consists of the following:
@@ -132,7 +95,7 @@
/// https://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&format=html
/// https://www.gnu.org/software/tar/manual/html_chapter/tar_15.html#SEC188
/// http://cdrtools.sourceforge.net/private/man/star/star.4.html
- static const v7 = TarFormat._internal(1);
+ v7(1, 'V7'),
/// USTAR (Unix Standard TAR) header format defined in POSIX.1-1988.
///
@@ -178,7 +141,7 @@
/// https://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&format=html
/// https://www.gnu.org/software/tar/manual/html_chapter/tar_15.html#SEC188
/// http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06
- static const ustar = TarFormat._internal(2);
+ ustar(2, 'USTAR'),
/// PAX header format defined in POSIX.1-2001.
///
@@ -196,7 +159,7 @@
/// https://www.gnu.org/software/tar/manual/html_chapter/tar_15.html#SEC188
/// http://cdrtools.sourceforge.net/private/man/star/star.4.html
/// http://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html
- static const pax = TarFormat._internal(4);
+ pax(4, 'PAX'),
/// GNU header format.
///
@@ -246,7 +209,7 @@
///
/// Reference:
/// https://www.gnu.org/software/tar/manual/html_node/Standard.html
- static const gnu = TarFormat._internal(8);
+ gnu(8, 'GNU'),
/// Schily's TAR format, which is incompatible with USTAR.
/// This does not cover STAR extensions to the PAX format; these fall under
@@ -284,5 +247,76 @@
///
/// Reference:
/// http://cdrtools.sourceforge.net/private/man/star/star.4.html
- static const star = TarFormat._internal(16);
+ star(16, 'STAR'),
+ ;
+
+ @override
+ final int _value;
+
+ final String _name;
+
+ const TarFormat(this._value, this._name);
+
+ @override
+ bool get valid => true;
+
+ @override
+ MaybeTarFormat operator |(TarFormat other) {
+ return other == this ? this : _MaybeTarFormat(_value | other._value);
+ }
+
+ @override
+ bool has(MaybeTarFormat other) {
+ return other == this;
+ }
+
+ @override
+ MaybeTarFormat mayOnlyBe(MaybeTarFormat other) {
+ return MaybeTarFormat._(_value & other._value);
+ }
+
+ @override
+ String toString() => _name;
+}
+
+final class _MaybeTarFormat implements MaybeTarFormat {
+ // Note: We never represent a single tar format in a _MaybeTarFormat, these
+ // are represented in the TarFormat enum.
+ @override
+ final int _value;
+
+ const _MaybeTarFormat(this._value);
+
+ @override
+ int get hashCode => _value;
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! TarFormat) return false;
+
+ return _value == other._value;
+ }
+
+ @override
+ String toString() {
+ if (!valid) return 'Invalid';
+
+ return TarFormat.values.where(has).map((e) => e._name).join(' or ');
+ }
+
+ @override
+ bool has(MaybeTarFormat other) => _value & other._value != 0;
+
+ @override
+ bool get valid => _value != 0;
+
+ @override
+ MaybeTarFormat mayOnlyBe(MaybeTarFormat other) {
+ return MaybeTarFormat._(_value & other._value);
+ }
+
+ @override
+ MaybeTarFormat operator |(TarFormat other) {
+ return MaybeTarFormat._(_value | other._value);
+ }
}
diff --git a/lib/src/header.dart b/lib/src/header.dart
index 2f35662..e18cddf 100644
--- a/lib/src/header.dart
+++ b/lib/src/header.dart
@@ -79,8 +79,7 @@
///
/// A tar header stores meta-information about the matching tar entry, such as
/// its name.
-@sealed
-abstract class TarHeader {
+sealed class TarHeader {
/// Type of header entry. In the V7 TAR format, this field was known as the
/// link flag.
TypeFlag get typeFlag;
@@ -125,7 +124,11 @@
int get devMinor;
/// The TAR format of the header.
- TarFormat get format;
+ ///
+ /// When this library is sure it knows the format of the tar entry, this will
+ /// be a [TarFormat] enum value. In other cases, a [MaybeTarFormat] could
+ /// represent multiple possible formats.
+ MaybeTarFormat get format;
/// Checks if this header indicates that the file will have content.
bool get hasContent {
@@ -226,11 +229,14 @@
int devMinor;
@override
- TarFormat format;
+ MaybeTarFormat format;
@override
TypeFlag get typeFlag {
- return internalTypeFlag == TypeFlag.regA ? TypeFlag.reg : internalTypeFlag;
+ return switch (internalTypeFlag) {
+ TypeFlag.regA => TypeFlag.reg, // normalize
+ final other => other,
+ };
}
/// This constructor is meant to help us deal with header-only headers (i.e.
@@ -281,7 +287,7 @@
throw TarException.header('Indicates an invalid size of $size');
}
- if (format.isValid() && format != TarFormat.v7) {
+ if (format.valid && format != TarFormat.v7) {
// If it's a valid header that is not of the v7 format, it will have the
// USTAR fields
header
@@ -370,7 +376,7 @@
/// Checks that [rawHeader] represents a valid tar header based on the
/// checksum, and then attempts to guess the specific format based
/// on magic values. If the checksum fails, then an error is thrown.
-TarFormat _getFormat(Uint8List rawHeader) {
+MaybeTarFormat _getFormat(Uint8List rawHeader) {
final checksum = rawHeader.readOctal(checksumOffset, checksumLength);
// Modern TAR archives use the unsigned checksum, but we check the signed
diff --git a/lib/src/reader.dart b/lib/src/reader.dart
index cf10987..c167151 100644
--- a/lib/src/reader.dart
+++ b/lib/src/reader.dart
@@ -19,8 +19,7 @@
/// It is designed to read from a stream and to spit out substreams for
/// individual file contents in order to minimize the amount of memory needed
/// to read each archive where possible.
-@sealed
-class TarReader implements StreamIterator<TarEntry> {
+final class TarReader implements StreamIterator<TarEntry> {
final BlockReader _reader;
final PaxHeaders _paxHeaders = PaxHeaders();
final int _maxSpecialFileSize;
@@ -677,7 +676,7 @@
}
@internal
-class PaxHeaders extends UnmodifiableMapBase<String, String> {
+final class PaxHeaders extends UnmodifiableMapBase<String, String> {
final Map<String, String> _globalHeaders = {};
Map<String, String> _localHeaders = {};
@@ -873,7 +872,7 @@
///
/// Draining this stream will set the [TarReader._currentStream] field back to
/// null. There can only be one content stream at the time.
-class _CurrentEntryStream extends Stream<List<int>> {
+final class _CurrentEntryStream extends Stream<List<int>> {
_EntryStreamState state = _EntryStreamState.preListen;
final TarReader _reader;
@@ -1008,6 +1007,6 @@
_listener.addError(
TarException('Unexpected end of tar file'), StackTrace.current);
}
- _listener.close();
+ unawaited(_listener.close());
}
}
diff --git a/lib/src/sparse.dart b/lib/src/sparse.dart
index 35c0311..bb938d0 100644
--- a/lib/src/sparse.dart
+++ b/lib/src/sparse.dart
@@ -10,7 +10,7 @@
/// [SparseEntry]s can represent either data or holes, and we can easily
/// convert between the two if we know the size of the file, all the sparse
/// data and all the sparse entries combined must give the full size.
-class SparseEntry {
+final class SparseEntry {
final int offset;
final int length;
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index e2bb5b6..1ea5506 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -241,7 +241,7 @@
}
/// An optimized reader reading 512-byte blocks from an input stream.
-class BlockReader {
+final class BlockReader {
final Stream<List<int>> _input;
StreamSubscription<List<int>>? _subscription;
bool _isClosed = false;
@@ -300,8 +300,13 @@
_outgoing = null;
_pause();
+ // Scheduling this in a microtask becuase the stream controller is
+ // synchronous.
scheduleMicrotask(() {
- outgoing.close();
+ // We don't need to await this since the stream controller is not used
+ // afterwards, if there's a paused listener we don't really care about
+ // that.
+ unawaited(outgoing.close());
});
return true;
} else if (outgoing.isPaused || outgoing.isClosed) {
@@ -395,8 +400,14 @@
}
_isClosed = true;
- _subscription?.cancel();
- outgoing.close();
+
+ // Can be unawated because this is an onDone callback of the subscription,
+ // the subscription is already complete and we're just cleaning up.
+ unawaited(_subscription?.cancel());
+
+ // Can be unawated because we're fully done here, we won't do anything else
+ // with the outgoing controller.
+ unawaited(outgoing.close());
}
void _subscribeOrResume() {
diff --git a/lib/src/writer.dart b/lib/src/writer.dart
index 5ff92b9..17cab90 100644
--- a/lib/src/writer.dart
+++ b/lib/src/writer.dart
@@ -9,7 +9,8 @@
import 'header.dart';
import 'utils.dart';
-class _WritingTransformer extends StreamTransformerBase<TarEntry, List<int>> {
+final class _WritingTransformer
+ extends StreamTransformerBase<TarEntry, List<int>> {
final OutputFormat format;
const _WritingTransformer(this.format);
@@ -19,7 +20,10 @@
// sync because the controller proxies another stream
final controller = StreamController<List<int>>(sync: true);
controller.onListen = () {
- stream.pipe(tarWritingSink(controller, format: format));
+ // Can be unawaited since it's the only thing done in onListen and since
+ // pipe is a terminal operation managing the remaining lifecycle of this
+ // stream controller.
+ unawaited(stream.pipe(tarWritingSink(controller, format: format)));
};
return controller.stream;
@@ -152,7 +156,7 @@
gnuLongName,
}
-class _WritingSink extends StreamSink<TarEntry> {
+final class _WritingSink extends StreamSink<TarEntry> {
final StreamSink<List<int>> _output;
final _SynchronousTarSink _synchronousWriter;
bool _closed = false;
@@ -247,7 +251,7 @@
return Uint8List(padding);
}
-class _SynchronousTarConverter
+final class _SynchronousTarConverter
extends Converter<SynchronousTarEntry, List<int>> {
final OutputFormat format;
@@ -269,7 +273,7 @@
}
}
-class _SynchronousTarSink extends Sink<SynchronousTarEntry> {
+final class _SynchronousTarSink extends Sink<SynchronousTarEntry> {
final OutputFormat _format;
final Sink<List<int>> _output;
diff --git a/pubspec.yaml b/pubspec.yaml
index 7e6cd4e..3164d39 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,10 +1,10 @@
name: tar
description: Memory-efficient, streaming implementation of the tar file format
-version: 0.5.6
+version: 1.0.0
repository: https://github.com/simolus3/tar/
environment:
- sdk: '>=2.12.0 <3.0.0'
+ sdk: '>=3.0.0 <4.0.0'
dependencies:
async: ^2.6.0
@@ -13,7 +13,7 @@
dev_dependencies:
charcode: ^1.2.0
- extra_pedantic: ^3.0.0
+ extra_pedantic: ^4.0.0
file: ^6.1.2
node_io: ^2.1.0
path: ^1.8.0
diff --git a/test/format_test.dart b/test/format_test.dart
new file mode 100644
index 0000000..c42d68f
--- /dev/null
+++ b/test/format_test.dart
@@ -0,0 +1,43 @@
+import 'package:tar/tar.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('operator |', () {
+ expect(TarFormat.gnu | TarFormat.star, _isFormat('GNU or STAR'));
+ expect(TarFormat.star | TarFormat.gnu, _isFormat('GNU or STAR'));
+
+ expect(TarFormat.v7 | TarFormat.pax | TarFormat.star,
+ _isFormat('V7 or PAX or STAR'));
+ });
+
+ test('has', () {
+ expect(TarFormat.gnu.has(TarFormat.gnu), isTrue);
+ expect(TarFormat.gnu.has(TarFormat.v7), isFalse);
+
+ expect(TarFormat.gnu.has(TarFormat.v7 | TarFormat.gnu), isFalse);
+ expect((TarFormat.v7 | TarFormat.gnu).has(TarFormat.gnu), isTrue);
+ });
+
+ test('mayOnlyBe', () {
+ expect(TarFormat.gnu.mayOnlyBe(TarFormat.v7), _isInvalid);
+ expect(TarFormat.gnu.mayOnlyBe(TarFormat.gnu), TarFormat.gnu);
+
+ expect((TarFormat.gnu | TarFormat.pax).mayOnlyBe(TarFormat.pax),
+ TarFormat.pax);
+ expect(
+ (TarFormat.gnu | TarFormat.pax | TarFormat.v7)
+ .mayOnlyBe(TarFormat.pax | TarFormat.v7),
+ _isFormat('V7 or PAX'),
+ );
+ expect((TarFormat.gnu | TarFormat.pax).mayOnlyBe(TarFormat.v7), _isInvalid);
+ });
+}
+
+TypeMatcher<MaybeTarFormat> _isFormat(String representation) {
+ return isA<MaybeTarFormat>()
+ .having((e) => e.toString(), 'toString()', representation);
+}
+
+TypeMatcher<MaybeTarFormat> get _isInvalid {
+ return _isFormat('Invalid').having((e) => e.valid, 'valid', isFalse);
+}
diff --git a/test/reader_test.dart b/test/reader_test.dart
index 75aa18c..11605b2 100644
--- a/test/reader_test.dart
+++ b/test/reader_test.dart
@@ -10,6 +10,8 @@
import 'utils.dart';
+// ignore_for_file: discarded_futures
+
void main() {
group('POSIX.1-2001', () {
test('reads files', () => _testWith('reference/posix.tar'));
diff --git a/test/sparse_test.dart b/test/sparse_test.dart
index 6729ae0..407f9d7 100644
--- a/test/sparse_test.dart
+++ b/test/sparse_test.dart
@@ -11,6 +11,8 @@
import 'system_tar.dart';
+// ignore_for_file: discarded_futures
+
/// Writes [size] random bytes to [path].
Future<void> createTestFile(String path, int size) {
final random = Random();
diff --git a/test/utils_test.dart b/test/utils_test.dart
index e9b3a8b..59b52ca 100644
--- a/test/utils_test.dart
+++ b/test/utils_test.dart
@@ -38,12 +38,13 @@
const lengths = [024 * 1024 * 128 + 12, 12, 0];
for (final length in lengths) {
- test('with length $length', () {
+ test('with length $length', () async {
final stream = zeroes(length);
expect(
- stream.fold<int>(0, (previous, element) => previous + element.length),
- completion(length),
+ await stream.fold<int>(
+ 0, (previous, element) => previous + element.length),
+ length,
);
});
}
diff --git a/test/writer_test.dart b/test/writer_test.dart
index a026432..040d2fc 100644
--- a/test/writer_test.dart
+++ b/test/writer_test.dart
@@ -143,6 +143,7 @@
void shouldThrow(tar.TarEntry entry) {
final output = tar.tarWritingSink(_NullStreamSink(),
format: tar.OutputFormat.gnuLongName);
+ // ignore: discarded_futures
expect(Stream.value(entry).pipe(output), throwsA(isUnsupportedError));
}