blob: 33fb5913522be54e43fd39922c0f51ddd4e44847 [file] [log] [blame]
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;
}
}