tarArchive function

Uint8List tarArchive(
  1. List<TarFile> files, {
  2. DateTime? modified,
})

Writes files as a ustar archive, the format Docker's PUT /containers/{id}/archive unpacks.

Only regular files, owned by root, with the time modified; the directories above a file are created by the daemon when it unpacks. A path longer than ustar's 100 characters is split into prefix and name at a /; one that cannot be split throws an ArgumentError.

Testcontainers (Java): TarArchiveOutputStream of commons-compress, in copyFileToContainer.

Implementation

Uint8List tarArchive(List<TarFile> files, {DateTime? modified}) {
  final time = (modified ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000;
  final out = BytesBuilder(copy: false);
  for (final file in files) {
    final (prefix, name) = _split(file.path.replaceFirst(RegExp('^/+'), ''));
    final header = Uint8List(512);
    void put(int offset, int length, String text) {
      final bytes = utf8.encode(text);
      header.setRange(offset, offset + bytes.length.clamp(0, length), bytes);
    }

    String octal(int value, int digits) =>
        '${value.toRadixString(8).padLeft(digits - 1, '0')}\u0000';

    put(0, 100, name);
    put(100, 8, octal(file.mode & 0xFFF, 8));
    put(108, 8, octal(0, 8));
    put(116, 8, octal(0, 8));
    put(124, 12, octal(file.bytes.length, 12));
    put(136, 12, octal(time, 12));
    put(148, 8, '        '); // the checksum counts these as spaces
    put(156, 1, '0');
    put(257, 6, 'ustar\u0000');
    put(263, 2, '00');
    put(345, 155, prefix);
    final sum = header.fold<int>(0, (a, b) => a + b);
    put(148, 8, '${sum.toRadixString(8).padLeft(6, '0')}\u0000 ');
    out
      ..add(header)
      ..add(file.bytes);
    final padding = (512 - file.bytes.length % 512) % 512;
    if (padding > 0) {
      out.add(Uint8List(padding));
    }
  }
  out.add(Uint8List(1024)); // the end of the archive
  return out.takeBytes();
}