// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:collection';
import 'dart:convert';
import 'dart:io' as io;

import 'package:archive/archive.dart' as a;
import 'package:path/path.dart' as path;

import 'cache.dart';
import 'limits.dart';
import 'patterns.dart';

enum FileType {
  binary, // won't have its own license block
  text, // might have its own UTF-8 license block
  latin1Text, // might have its own Windows-1252 license block
  zip, // should be parsed as an archive and drilled into
  tar, // should be parsed as an archive and drilled into
  gz, // should be parsed as a single compressed file and exposed
  bzip2, // should be parsed as a single compressed file and exposed
  notPartOfBuild, // can be skipped entirely (e.g. Mac OS X ._foo files, bash scripts)
}

typedef Reader = List<int> Function();

class BytesOf extends Key { BytesOf(super.value); }
class UTF8Of extends Key { UTF8Of(super.value); }
class Latin1Of extends Key { Latin1Of(super.value); }

bool matchesSignature(List<int> bytes, List<int> signature) {
  if (bytes.length < signature.length) {
    return false;
  }
  for (int index = 0; index < signature.length; index += 1) {
    if (signature[index] != -1 && bytes[index] != signature[index]) {
      return false;
    }
  }
  return true;
}

bool hasSubsequence(List<int> bytes, List<int> signature, int limit) {
  if (bytes.length < limit) {
    limit = bytes.length;
  }
  for (int index = 0; index < limit; index += 1) {
    if (bytes.length - index < signature.length) {
      return false;
    }
    for (int offset = 0; offset < signature.length; offset += 1) {
      if (signature[offset] != -1 && bytes[index + offset] != signature[offset]) {
        break;
      }
      if (offset + 1 == signature.length) {
        return true;
      }
    }
  }
  return false;
}

const String kMultiLicenseFileHeader = 'Notices for files contained in';

bool isMultiLicenseNotice(Reader reader) {
  final List<int> bytes = reader();
  return ascii.decode(bytes.take(kMultiLicenseFileHeader.length).toList(), allowInvalid: true) == kMultiLicenseFileHeader;
}

FileType identifyFile(String name, Reader reader) {
  List<int>? bytes;
  if ((path.split(name).reversed.take(6).toList().reversed.join('/') == 'third_party/icu/source/extra/uconv/README') || // This specific ICU README isn't in UTF-8.
      (path.split(name).reversed.take(6).toList().reversed.join('/') == 'third_party/icu/source/samples/uresb/sr.txt') || // This specific sample contains non-UTF-8 data (unlike other sr.txt files).
      (path.split(name).reversed.take(2).toList().reversed.join('/') == 'builds/detect.mk') || // This specific freetype sample contains non-UTF-8 data (unlike other .mk files).
      (path.split(name).reversed.take(3).toList().reversed.join('/') == 'third_party/cares/cares.rc')) {
    return FileType.latin1Text;
  }
  if (path.split(name).reversed.take(6).toList().reversed.join('/') == 'dart/runtime/tests/vm/dart/bad_snapshot') { // Not any particular format
    return FileType.binary;
  }
  if (path.split(name).reversed.take(9).toList().reversed.join('/') == 'fuchsia/sdk/linux/dart/zircon/lib/src/fakes/handle_disposition.dart' || // has bogus but benign "authors" reference, reported to jamesr@
      path.split(name).reversed.take(6).toList().reversed.join('/') == 'third_party/angle/src/common/fuchsia_egl/fuchsia_egl.c' || // has bogus but benign "authors" reference, reported to author and legal team
      path.split(name).reversed.take(6).toList().reversed.join('/') == 'third_party/angle/src/common/fuchsia_egl/fuchsia_egl.h' || // has bogus but benign "authors" reference, reported to author and legal team
      path.split(name).reversed.take(6).toList().reversed.join('/') == 'third_party/angle/src/common/fuchsia_egl/fuchsia_egl_backend.h') { // has bogus but benign "authors" reference, reported to author and legal team
    return FileType.binary;
  }
  final String base = path.basename(name);
  if (base.startsWith('._')) {
    bytes ??= reader();
    if (matchesSignature(bytes, <int>[0x00, 0x05, 0x16, 0x07, 0x00, 0x02, 0x00, 0x00, 0x4d, 0x61, 0x63, 0x20, 0x4f, 0x53, 0x20, 0x58])) {
      return FileType.notPartOfBuild;
    } // The ._* files in Mac OS X archives that gives icons and stuff
  }
  if (path.split(name).contains('cairo')) {
    bytes ??= reader();
    // "Copyright <latin1 copyright symbol> "
    if (hasSubsequence(bytes, <int>[0x43, 0x6f, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0xA9, 0x20], kMaxSize)) {
      return FileType.latin1Text;
    }
  }
  switch (base) {
    // Build files
    case 'DEPS': return FileType.text;
    case 'MANIFEST': return FileType.text;
    // Licenses
    case 'COPYING': return FileType.text;
    case 'LICENSE': return FileType.text;
    case 'NOTICE.txt': return isMultiLicenseNotice(reader) ? FileType.binary : FileType.text;
    case 'NOTICE': return FileType.text;
    // Documentation
    case 'Changes': return FileType.text;
    case 'change.log': return FileType.text;
    case 'ChangeLog': return FileType.text;
    case 'CHANGES.0': return FileType.latin1Text;
    case 'README': return FileType.text;
    case 'TODO': return FileType.text;
    case 'NEWS': return FileType.text;
    case 'README.chromium': return FileType.text;
    case 'README.flutter': return FileType.text;
    case 'README.tests': return FileType.text;
    case 'OWNERS': return FileType.text;
    case 'AUTHORS': return FileType.text;
    // Signatures (found in .jar files typically)
    case 'CERT.RSA': return FileType.binary;
    case 'ECLIPSE_.RSA': return FileType.binary;
    // Binary data files
    case 'tzdata': return FileType.binary;
    case 'compressed_atrace_data.txt': return FileType.binary;
    // Source files that don't use UTF-8
    case 'Messages_de_DE.properties': // has a few non-ASCII characters they forgot to escape (from gnu-libstdc++)
    case 'mmx_blendtmp.h': // author name in comment contains latin1 (mesa)
    case 'calling_convention.txt': // contains a soft hyphen instead of a real hyphen for some reason (mesa)
    // Character encoding data files
    case 'danish-ISO-8859-1.txt':
    case 'eucJP.txt':
    case 'hangul-eucKR.txt':
    case 'hania-eucKR.txt':
    case 'ibm-37-test.txt':
    case 'iso8859-1.txt':
    case 'ISO-8859-2.txt':
    case 'ISO-8859-3.txt':
    case 'koi8r.txt':
      return FileType.latin1Text;
    // Giant data files
    case 'icudtl_dat.S':
    case 'icudtl.dat':
    case 'icudtl.dat.hash':
      return FileType.binary;
  }
  switch (path.extension(name)) {
    // C/C++ code
    case '.h': return FileType.text;
    case '.c': return FileType.text;
    case '.cc': return FileType.text;
    case '.cpp': return FileType.text;
    case '.inc': return FileType.text;
    // Go code
    case '.go': return FileType.text;
    // ObjectiveC code
    case '.m': return FileType.text;
    // Assembler
    case '.asm': return FileType.text;
    // Shell
    case '.sh': return FileType.notPartOfBuild;
    case '.bat': return FileType.notPartOfBuild;
    // Build files
    case '.ac': return FileType.notPartOfBuild;
    case '.am': return FileType.notPartOfBuild;
    case '.gn': return FileType.notPartOfBuild;
    case '.gni': return FileType.notPartOfBuild;
    case '.gyp': return FileType.notPartOfBuild;
    case '.gypi': return FileType.notPartOfBuild;
    // Java code
    case '.java': return FileType.text;
    case '.jar': return FileType.zip; // Java package
    case '.class': return FileType.binary; // compiled Java bytecode (usually found inside .jar archives)
    case '.dex': return FileType.binary; // Dalvik Executable (usually found inside .jar archives)
    // Dart code
    case '.dart': return FileType.text;
    case '.dill': return FileType.binary; // Compiled Dart code
    // LLVM bitcode
    case '.bc': return FileType.binary;
    // Python code
    case '.py':
      bytes ??= reader();
      // # -*- coding: Latin-1 -*-
      if (matchesSignature(bytes, <int>[0x23, 0x20, 0x2d, 0x2a, 0x2d, 0x20, 0x63, 0x6f, 0x64,
                                        0x69, 0x6e, 0x67, 0x3a, 0x20, 0x4c, 0x61, 0x74, 0x69,
                                        0x6e, 0x2d, 0x31, 0x20, 0x2d, 0x2a, 0x2d])) {
        return FileType.latin1Text;
      }
      return FileType.text;
    case '.pyc': return FileType.binary; // compiled Python bytecode
    // Machine code
    case '.so': return FileType.binary; // ELF shared object
    case '.xpt': return FileType.binary; // XPCOM Type Library
    // Graphics code
    case '.glsl': return FileType.text;
    case '.spvasm': return FileType.text;
    // Documentation
    case '.md': return FileType.text;
    case '.txt': return FileType.text;
    case '.html': return FileType.text;
    // Fonts
    case '.ttf': return FileType.binary; // TrueType Font
    case '.ttcf': // (mac)
    case '.ttc': return FileType.binary; // TrueType Collection (windows)
    case '.woff': return FileType.binary; // Web Open Font Format
    case '.otf': return FileType.binary; // OpenType Font
    // Graphics formats
    case '.gif': return FileType.binary; // GIF
    case '.png': return FileType.binary; // PNG
    case '.tga': return FileType.binary; // Truevision TGA (TARGA)
    case '.dng': return FileType.binary; // Digial Negative (Adobe RAW format)
    case '.jpg':
    case '.jpeg': return FileType.binary; // JPEG
    case '.ico': return FileType.binary; // Windows icon format
    case '.icns': return FileType.binary; // macOS icon format
    case '.bmp': return FileType.binary; // Windows bitmap format
    case '.wbmp': return FileType.binary; // Wireless bitmap format
    case '.webp': return FileType.binary; // WEBP
    case '.pdf': return FileType.binary; // PDF
    case '.emf': return FileType.binary; // Windows enhanced metafile format
    case '.skp': return FileType.binary; // Skia picture format
    case '.mskp': return FileType.binary; // Skia picture format
    case '.spv': return FileType.binary; // SPIR-V
    // Videos
    case '.ogg': return FileType.binary; // Ogg media
    case '.mp4': return FileType.binary; // MPEG media
    case '.ts': return FileType.binary; // MPEG2 transport stream
    // Other binary files
    case '.raw': return FileType.binary; // raw audio or graphical data
    case '.bin': return FileType.binary; // some sort of binary data
    case '.rsc': return FileType.binary; // some sort of resource data
    case '.arsc': return FileType.binary; // Android compiled resources
    case '.apk': return FileType.zip; // Android Package
    case '.crx': return FileType.binary; // Chrome extension
    case '.keystore': return FileType.binary;
    case '.icc': return FileType.binary; // Color profile
    case '.swp': return FileType.binary; // Vim swap file
    case '.bfbs': return FileType.binary; // Flatbuffers Binary Schema
    // Archives
    case '.zip': return FileType.zip; // ZIP
    case '.tar': return FileType.tar; // Tar
    case '.gz': return FileType.gz; // GZip
    case '.bzip2': return FileType.bzip2; // BZip2
    // Image file types from the Fuchsia SDK.
    case '.blk':
    case '.vboot':
    case '.snapshot':
    case '.zbi':
      return FileType.binary;
    // Special cases
    case '.patch':
    case '.diff':
      // Don't try to read the copyright out of patch files, since there'll be fragments.
      return FileType.binary;
    case '.plist':
      // These commonly include the word "copyright" but in a way that isn't necessarily a copyright statement that applies to the file.
      // Since there's so few of them, and none have their own copyright statement, we just treat them as binary files.
      return FileType.binary;
  }
  bytes ??= reader();
  assert(bytes.isNotEmpty);
  if (matchesSignature(bytes, <int>[0x1F, 0x8B])) {
    return FileType.gz;
  } // GZip archive
  if (matchesSignature(bytes, <int>[0x42, 0x5A])) {
    return FileType.bzip2;
  } // BZip2 archive
  if (matchesSignature(bytes, <int>[0x42, 0x43])) {
    return FileType.binary;
  } // LLVM Bitcode
  if (matchesSignature(bytes, <int>[0xAC, 0xED])) {
    return FileType.binary;
  } // Java serialized object
  if (matchesSignature(bytes, <int>[0x4D, 0x5A])) {
    return FileType.binary;
  } // MZ executable (DOS, Windows PEs, etc)
  if (matchesSignature(bytes, <int>[0xFF, 0xD8, 0xFF])) {
    return FileType.binary;
  } // JPEG
  if (matchesSignature(bytes, <int>[-1, -1, 0xda, 0x27])) {
    return FileType.binary;
  } // ICU data files (.brk, .dict, etc)
  if (matchesSignature(bytes, <int>[0x03, 0x00, 0x08, 0x00])) {
    return FileType.binary;
  } // Android Binary XML
  if (matchesSignature(bytes, <int>[0x25, 0x50, 0x44, 0x46])) {
    return FileType.binary;
  } // PDF
  if (matchesSignature(bytes, <int>[0x43, 0x72, 0x32, 0x34])) {
    return FileType.binary;
  } // Chrome extension
  if (matchesSignature(bytes, <int>[0x4F, 0x67, 0x67, 0x53])) {
    return FileType.binary;
  } // Ogg media
  if (matchesSignature(bytes, <int>[0x50, 0x4B, 0x03, 0x04])) {
    return FileType.zip;
  } // ZIP archive
  if (matchesSignature(bytes, <int>[0x7F, 0x45, 0x4C, 0x46])) {
    return FileType.binary;
  } // ELF
  if (matchesSignature(bytes, <int>[0xCA, 0xFE, 0xBA, 0xBE])) {
    return FileType.binary;
  } // compiled Java bytecode (usually found inside .jar archives)
  if (matchesSignature(bytes, <int>[0xCE, 0xFA, 0xED, 0xFE])) {
    return FileType.binary;
  } // Mach-O binary, 32 bit, reverse byte ordering scheme
  if (matchesSignature(bytes, <int>[0xCF, 0xFA, 0xED, 0xFE])) {
    return FileType.binary;
  } // Mach-O binary, 64 bit, reverse byte ordering scheme
  if (matchesSignature(bytes, <int>[0xFE, 0xED, 0xFA, 0xCE])) {
    return FileType.binary;
  } // Mach-O binary, 32 bit
  if (matchesSignature(bytes, <int>[0xFE, 0xED, 0xFA, 0xCF])) {
    return FileType.binary;
  } // Mach-O binary, 64 bit
  if (matchesSignature(bytes, <int>[0x75, 0x73, 0x74, 0x61, 0x72])) {
    return FileType.bzip2;
  } // Tar
  if (matchesSignature(bytes, <int>[0x47, 0x49, 0x46, 0x38, 0x37, 0x61])) {
    return FileType.binary;
  } // GIF87a
  if (matchesSignature(bytes, <int>[0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
    return FileType.binary;
  } // GIF89a
  if (matchesSignature(bytes, <int>[0x64, 0x65, 0x78, 0x0A, 0x30, 0x33, 0x35, 0x00])) {
    return FileType.binary;
  } // Dalvik Executable
  if (matchesSignature(bytes, <int>[0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A])) {
    // TODO(ianh): implement .ar parser, https://github.com/flutter/flutter/issues/25633
    return FileType.binary; // Unix archiver (ar)
  }
  if (matchesSignature(bytes, <int>[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a])) {
    return FileType.binary;
  } // PNG
  if (matchesSignature(bytes, <int>[0x58, 0x50, 0x43, 0x4f, 0x4d, 0x0a, 0x54, 0x79, 0x70, 0x65, 0x4c, 0x69, 0x62, 0x0d, 0x0a, 0x1a])) {
    return FileType.binary;
  } // XPCOM Type Library
  if (matchesSignature(bytes, <int>[0x23, 0x21])) {
    // #! indicates a shell script, those are not part of the build
    return FileType.notPartOfBuild;
  }
  return FileType.text;
}

String _normalize(String fileContents) {
  fileContents = fileContents.replaceAll(newlinePattern, '\n');
  fileContents = fileContents.replaceAll('\t', ' ' * 4);
  return fileContents;
}


// INTERFACE

// base class
abstract class IoNode {
  // Subclasses of IoNode are not mutually exclusive.
  // For example, a ZIP file is represented as a File that also implements Directory.
  String get name;
  String get fullName;

  @override
  String toString() => fullName;
}

// interface
abstract class File extends IoNode {
  List<int>? readBytes();
}

// interface
abstract class TextFile extends File {
  String readString();
}

mixin UTF8TextFile implements TextFile {
  @override
  String readString() {
    try {
      return cache(UTF8Of(this), () => _normalize(utf8.decode(readBytes()!)));
    } on FormatException {
      print(fullName);
      rethrow;
    }
  }
}

mixin Latin1TextFile implements TextFile {
  @override
  String readString() {
    return cache(Latin1Of(this), () {
      final List<int> bytes = readBytes()!;
      if (bytes.any((int byte) => byte == 0x00)) {
        throw '$fullName contains a U+0000 NULL and is probably not actually encoded as Win1252';
      }
      bool isUTF8 = false;
      try {
        cache(UTF8Of(this), () => utf8.decode(readBytes()!));
        isUTF8 = true;
      } on FormatException {
        // Exceptions are fine/expected for non-UTF8 text, which we test for
        // immediately below.
      }
      if (isUTF8) {
        throw '$fullName contains valid UTF-8 and is probably not actually encoded as Win1252';
      }
      return _normalize(latin1.decode(bytes));
    });
  }
}

// interface
abstract class Directory extends IoNode {
  // lists children (shallow walk, not deep walk)
  Iterable<IoNode> get walk;
}

// interface
abstract class Link extends IoNode { }

mixin ZipFile on File implements Directory {
  ArchiveDirectory? _root;

  @override
  Iterable<IoNode> get walk {
    try {
      _root ??= ArchiveDirectory.parseArchive(a.ZipDecoder().decodeBytes(readBytes()!), fullName);
      return _root!.walk;
    } catch (exception) {
      print('failed to parse archive:\n$fullName');
      rethrow;
    }
  }
}

mixin TarFile on File implements Directory {
  ArchiveDirectory? _root;

  @override
  Iterable<IoNode> get walk {
    try {
      _root ??= ArchiveDirectory.parseArchive(a.TarDecoder().decodeBytes(readBytes()!), fullName);
      return _root!.walk;
    } catch (exception) {
      print('failed to parse archive:\n$fullName');
      rethrow;
    }
  }
}

mixin GZipFile on File implements Directory {
  InMemoryFile? _data;

  @override
  Iterable<IoNode> get walk sync* {
    try {
      final String innerName = path.basenameWithoutExtension(fullName);
      _data ??= InMemoryFile.parse('$fullName!$innerName', a.GZipDecoder().decodeBytes(readBytes()!))!;
      if (_data != null) {
        yield _data!;
      }
    } catch (exception) {
      print('failed to parse archive:\n$fullName');
      rethrow;
    }
  }
}

mixin BZip2File on File implements Directory {
  InMemoryFile? _data;

  @override
  Iterable<IoNode> get walk sync* {
    try {
      final String innerName = path.basenameWithoutExtension(fullName);
      _data ??= InMemoryFile.parse('$fullName!$innerName', a.BZip2Decoder().decodeBytes(readBytes()!))!;
      if (_data != null) {
        yield _data!;
      }
    } catch (exception) {
      print('failed to parse archive:\n$fullName');
      rethrow;
    }
  }
}


// FILESYSTEM IMPLEMENTATIoN

class FileSystemDirectory extends IoNode implements Directory {
  FileSystemDirectory(this._directory);

  factory FileSystemDirectory.fromPath(String name) {
    return FileSystemDirectory(io.Directory(name));
  }

  final io.Directory _directory;

  @override
  String get name => path.basename(_directory.path);

  @override
  String get fullName => _directory.path;

  List<int> _readBytes(io.File file) {
    return cache(BytesOf(file), () => file.readAsBytesSync());
  }

  @override
  Iterable<IoNode> get walk sync* {
    final List<io.FileSystemEntity> list = _directory.listSync().toList();
    list.sort((io.FileSystemEntity a, io.FileSystemEntity b) => a.path.compareTo(b.path));
    for (final io.FileSystemEntity entity in list) {
      if (entity is io.Directory) {
        yield FileSystemDirectory(entity);
      } else if (entity is io.Link) {
        yield FileSystemLink(entity);
      } else {
        assert(entity is io.File);
        final io.File fileEntity = entity as io.File;
        if (fileEntity.lengthSync() > 0) {
          switch (identifyFile(fileEntity.path, () => _readBytes(fileEntity))) {
            case FileType.binary: yield FileSystemFile(fileEntity);
            case FileType.zip: yield FileSystemZipFile(fileEntity);
            case FileType.tar: yield FileSystemTarFile(fileEntity);
            case FileType.gz: yield FileSystemGZipFile(fileEntity);
            case FileType.bzip2: yield FileSystemBZip2File(fileEntity);
            case FileType.text: yield FileSystemUTF8TextFile(fileEntity);
            case FileType.latin1Text: yield FileSystemLatin1TextFile(fileEntity);
            case FileType.notPartOfBuild: break; // ignore this file
          }
        }
      }
    }
  }
}

class FileSystemLink extends IoNode implements Link {
  FileSystemLink(this._link);

  final io.Link _link;

  @override
  String get name => path.basename(_link.path);

  @override
  String get fullName => _link.path;
}

class FileSystemFile extends IoNode implements File {
  FileSystemFile(this._file);

  final io.File _file;

  @override
  String get name => path.basename(_file.path);

  @override
  String get fullName => _file.path;

  @override
  List<int> readBytes() {
    return cache(BytesOf(_file), () => _file.readAsBytesSync());
  }
}

class FileSystemUTF8TextFile extends FileSystemFile with UTF8TextFile {
  FileSystemUTF8TextFile(super.file);
}

class FileSystemLatin1TextFile extends FileSystemFile with Latin1TextFile {
  FileSystemLatin1TextFile(super.file);
}

class FileSystemZipFile extends FileSystemFile with ZipFile {
  FileSystemZipFile(super.file);
}

class FileSystemTarFile extends FileSystemFile with TarFile {
  FileSystemTarFile(super.file);
}

class FileSystemGZipFile extends FileSystemFile with GZipFile {
  FileSystemGZipFile(super.file);
}

class FileSystemBZip2File extends FileSystemFile with BZip2File {
  FileSystemBZip2File(super.file);
}


// ARCHIVES

class ArchiveDirectory extends IoNode implements Directory {
  ArchiveDirectory(this.fullName, this.name);

  @override
  final String fullName;

  @override
  final String name;

  final Map<String, ArchiveDirectory> _subdirectories = SplayTreeMap<String, ArchiveDirectory>();
  final List<ArchiveFile> _files = <ArchiveFile>[];

  void _add(a.ArchiveFile entry, List<String> remainingPath) {
    if (remainingPath.length > 1) {
      final String subdirectoryName = remainingPath.removeAt(0);
      _subdirectories.putIfAbsent(
        subdirectoryName,
        () => ArchiveDirectory('$fullName/$subdirectoryName', subdirectoryName)
      )._add(entry, remainingPath);
    } else {
      if (entry.size > 0) {
        final String entryFullName = '$fullName/${path.basename(entry.name)}';
        switch (identifyFile(entry.name, () => entry.content as List<int>)) {
          case FileType.binary: _files.add(ArchiveFile(entryFullName, entry));
          case FileType.zip: _files.add(ArchiveZipFile(entryFullName, entry));
          case FileType.tar: _files.add(ArchiveTarFile(entryFullName, entry));
          case FileType.gz: _files.add(ArchiveGZipFile(entryFullName, entry));
          case FileType.bzip2: _files.add(ArchiveBZip2File(entryFullName, entry));
          case FileType.text: _files.add(ArchiveUTF8TextFile(entryFullName, entry));
          case FileType.latin1Text: _files.add(ArchiveLatin1TextFile(entryFullName, entry));
          case FileType.notPartOfBuild: break; // ignore this file
        }
      }
    }
  }

  static ArchiveDirectory parseArchive(a.Archive archive, String ownerPath) {
    final ArchiveDirectory root = ArchiveDirectory('$ownerPath!', '');
    for (final a.ArchiveFile file in archive.files) {
      if (file.size > 0) {
        root._add(file, file.name.split('/'));
      }
    }
    return root;
  }

  @override
  Iterable<IoNode> get walk sync* {
    yield* _subdirectories.values;
    yield* _files;
  }
}

class ArchiveFile extends IoNode implements File {
  ArchiveFile(this.fullName, this._file);

  final a.ArchiveFile _file;

  @override
  String get name => path.basename(_file.name);

  @override
  final String fullName;

  @override
  List<int>? readBytes() {
    return _file.content as List<int>?;
  }
}

class ArchiveUTF8TextFile extends ArchiveFile with UTF8TextFile {
  ArchiveUTF8TextFile(super.fullName, super.file);
}

class ArchiveLatin1TextFile extends ArchiveFile with Latin1TextFile {
  ArchiveLatin1TextFile(super.fullName, super.file);
}

class ArchiveZipFile extends ArchiveFile with ZipFile {
  ArchiveZipFile(super.fullName, super.file);
}

class ArchiveTarFile extends ArchiveFile with TarFile {
  ArchiveTarFile(super.fullName, super.file);
}

class ArchiveGZipFile extends ArchiveFile with GZipFile {
  ArchiveGZipFile(super.fullName, super.file);
}

class ArchiveBZip2File extends ArchiveFile with BZip2File {
  ArchiveBZip2File(super.fullName, super.file);
}


// IN-MEMORY FILES (e.g. contents of GZipped files)

class InMemoryFile extends IoNode implements File {
  InMemoryFile(this.fullName, this._bytes);

  static InMemoryFile? parse(String fullName, List<int> bytes) {
    if (bytes.isEmpty) {
      return null;
    }
    switch (identifyFile(fullName, () => bytes)) {
      case FileType.binary: return InMemoryFile(fullName, bytes);
      case FileType.zip: return InMemoryZipFile(fullName, bytes);
      case FileType.tar: return InMemoryTarFile(fullName, bytes);
      case FileType.gz: return InMemoryGZipFile(fullName, bytes);
      case FileType.bzip2: return InMemoryBZip2File(fullName, bytes);
      case FileType.text: return InMemoryUTF8TextFile(fullName, bytes);
      case FileType.latin1Text: return InMemoryLatin1TextFile(fullName, bytes);
      case FileType.notPartOfBuild: break; // ignore this file
    }
    assert(false);
    return null;
  }

  final List<int> _bytes;

  @override
  String get name => '<data>';

  @override
  final String fullName;

  @override
  List<int> readBytes() => _bytes;
}

class InMemoryUTF8TextFile extends InMemoryFile with UTF8TextFile {
  InMemoryUTF8TextFile(super.fullName, super.file);
}

class InMemoryLatin1TextFile extends InMemoryFile with Latin1TextFile {
  InMemoryLatin1TextFile(super.fullName, super.file);
}

class InMemoryZipFile extends InMemoryFile with ZipFile {
  InMemoryZipFile(super.fullName, super.file);
}

class InMemoryTarFile extends InMemoryFile with TarFile {
  InMemoryTarFile(super.fullName, super.file);
}

class InMemoryGZipFile extends InMemoryFile with GZipFile {
  InMemoryGZipFile(super.fullName, super.file);
}

class InMemoryBZip2File extends InMemoryFile with BZip2File {
  InMemoryBZip2File(super.fullName, super.file);
}
