| import 'dart:async'; |
| import 'dart:typed_data'; |
| |
| import 'package:charcode/charcode.dart'; |
| |
| import 'common.dart'; |
| |
| /// An entry in a tar file. |
| /// |
| /// 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 [Entry] is active at a time. |
| /// A [MemoryEntry] is stored entirely in memory, it can be listened to multiple |
| /// times. To read turn a regular entry into a [MemoryEntry], use |
| /// [Entry.readFully]. |
| class Entry extends Stream<List<int>> { |
| /// The parsed [Header] of this tar entry. |
| final Header header; |
| final Stream<List<int>> _dataStream; |
| |
| /// The name of this entry, as indicated in the header or a previous pax |
| /// entry. |
| String get name => header.name; |
| |
| /// The type of tar entry (file, directory, etc.). |
| FileType get type => header.type; |
| |
| /// The content size of this entry, in bytes. |
| int get size => header.size; |
| |
| /// Time of the last modification of this file, as indicated in the [header]. |
| DateTime get lastModified => header.lastModified; |
| |
| Entry(this.header, this._dataStream); |
| |
| @override |
| StreamSubscription<List<int>> listen(void Function(List<int> event)? onData, |
| {Function? onError, void Function()? onDone, bool? cancelOnError}) { |
| return _dataStream.listen(onData, |
| onError: onError, onDone: onDone, cancelOnError: cancelOnError); |
| } |
| |
| /// Reads this entry to memory, and then returns it as a [MemoryEntry]. |
| FutureOr<MemoryEntry> readFully() async { |
| final builder = BytesBuilder(copy: false); |
| await forEach(builder.add); |
| |
| return MemoryEntry._(header, builder.takeBytes()); |
| } |
| } |
| |
| /// A tar entry stored entirely in memory. |
| class MemoryEntry extends Entry { |
| /// The data stored in this tar entry. |
| final Uint8List data; |
| |
| MemoryEntry._(Header header, this.data) : super(header, Stream.value(data)); |
| |
| /// Constructs a memory entry from its [header] and the in-memory [data]. |
| factory MemoryEntry(Header header, List<int> data) { |
| return MemoryEntry._(header, data.asUint8List()); |
| } |
| |
| /// Returns `this`. |
| @override |
| MemoryEntry readFully() => this; |
| } |
| |
| class Header { |
| /// The name of the tar entry. |
| final String name; |
| |
| /// The file mode of the entry. |
| /// |
| /// This corresponds to `FileStat.mode` in `dart:io`. |
| final int mode; |
| |
| /// ID of the user creating this file, or `0` if unknown. |
| final int uid; |
| |
| /// Group-ID of the user creating this file, or `0` if unknown. |
| final int gid; |
| |
| /// Size of the entry, in bytes. |
| /// |
| /// When writing streams with unknown size, set this to a negative value. In |
| /// that case, the writer will determine the correct size. |
| final int size; |
| |
| /// The time where this file was last modified. |
| final DateTime lastModified; |
| |
| /// The checksum of this header itself. |
| /// |
| /// This library does not currently verify checksums, but the writer will |
| /// generate correct checksums. |
| final int checksum; |
| |
| /// The [FileType] of this entry. |
| final FileType type; |
| |
| /// The link target, if this entry is a link. |
| final String? linkName; |
| |
| /// The tar version field, should always be `0`. |
| final int version; |
| |
| /// Name of the user creating this file. |
| final String? userName; |
| |
| /// Name of the group creating this file. |
| final String? groupName; |
| |
| Header({ |
| required this.name, |
| required this.mode, |
| this.version = 0, |
| this.type = FileType.regular, |
| this.userName = 'root', |
| this.groupName = 'root', |
| this.uid = 0, |
| this.gid = 0, |
| this.size = -1, |
| this.checksum = 0, |
| this.linkName, |
| DateTime? lastModified, |
| }) : lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0); |
| |
| factory Header.fromBlock(Uint8List data, |
| {String? fileName, String? linkName}) { |
| if (data.length != blockSize) { |
| throw ArgumentError.value( |
| data, 'data', 'Must have a length of $blockSize'); |
| } |
| |
| var name = fileName ?? readZeroTerminated(data, 0, 100); |
| final mode = _readOctInt(data, 100, 8); |
| final uid = _readOctInt(data, 108, 8); |
| final gid = _readOctInt(data, 116, 8); |
| final size = _readOctInt(data, 124, 12); |
| final mtime = |
| DateTime.fromMillisecondsSinceEpoch(_readOctInt(data, 136, 12) * 1000); |
| final checksum = _readOctInt(data, 148, 8); |
| final type = const { |
| regtype: FileType.regular, |
| aregtype: FileType.regular, |
| linktype: FileType.link, |
| dirtype: FileType.directory, |
| globalExtended: FileType.globalExtended, |
| extendedHeader: FileType.extendedHeader, |
| gnuTypeLongLinkName: FileType.gnuLongLinkName, |
| gnuTypeLongName: FileType.gnuLongName, |
| }[data[156]] ?? |
| FileType.unsupported; |
| final nameLink = linkName ?? readZeroTerminated(data, 157, 100); |
| |
| var version = 0; |
| String? uname; |
| String? gname; |
| |
| if (data.hasUstarMagic) { |
| version = _readOctInt(data, 263, 2); |
| uname = readZeroTerminated(data, 265, 32); |
| gname = readZeroTerminated(data, 297, 32); |
| |
| if (fileName == null) { |
| // Try to append a prefix if the file name wasn't enforced |
| final prefix = readZeroTerminated(data, 345, 155); |
| if (prefix.isNotEmpty) { |
| name = '$prefix/$name'; |
| } |
| } |
| } |
| |
| return Header( |
| name: name, |
| mode: mode, |
| uid: uid, |
| gid: gid, |
| size: size, |
| lastModified: mtime, |
| checksum: checksum, |
| type: type, |
| linkName: nameLink, |
| version: version, |
| userName: uname, |
| groupName: gname, |
| ); |
| } |
| |
| static int _readOctInt(Uint8List data, int offset, int length) { |
| var result = 0; |
| var multiplier = 1; |
| |
| for (var i = length - 1; i >= 0; i--) { |
| final charCode = data[offset + i]; |
| // Some tar implementations add a \0 or space at the end, ignore that |
| if (charCode < $0 || charCode > $9) continue; |
| |
| final digit = charCode - $0; |
| result += digit * multiplier; |
| multiplier <<= 3; // Multiply by the base, 8 |
| } |
| |
| return result; |
| } |
| } |
| |
| enum FileType { |
| /// A regular file entry. |
| regular, |
| |
| /// A link to another entry in the tar file |
| link, |
| |
| /// An entry used to indicate directories |
| directory, |
| |
| /// An entry with an unknown typeflag. |
| unsupported, |
| |
| /// A synthentic entry storing the header of the next entry. |
| /// |
| /// No entry with this type will be reported by the reader, they're handled |
| /// internally. |
| extendedHeader, |
| |
| /// A synthentic entry storing headers of upcoming entries. |
| /// |
| /// No entry with this type will be reported by the reader, they're handled |
| /// internally. |
| globalExtended, |
| |
| /// A synthentic entry storing the name of the next entry. |
| /// |
| /// No entry with this type will be reported by the reader, they're handled |
| /// internally. |
| gnuLongName, |
| |
| /// A synthentic entry storing the link target of the next entry. |
| /// |
| /// No entry with this type will be reported by the reader, they're handled |
| /// internally. |
| gnuLongLinkName, |
| } |
| |
| extension on Uint8List { |
| bool get hasUstarMagic { |
| // Ensure that the header has "ustar" as magic bytes |
| for (var i = 0; i < magic.length; i++) { |
| if (this[257 + i] != magic[i]) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| } |