|  | 
|  | 1 | +// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file | 
|  | 2 | +// for details. All rights reserved. Use of this source code is governed by a | 
|  | 3 | +// BSD-style license that can be found in the LICENSE file. | 
|  | 4 | + | 
|  | 5 | +import 'dart:async'; | 
|  | 6 | +import 'dart:io'; | 
|  | 7 | + | 
|  | 8 | +import 'package:archive/archive.dart'; | 
|  | 9 | +import 'package:async/async.dart'; | 
|  | 10 | +import 'package:meta/meta.dart'; | 
|  | 11 | +import 'package:path/path.dart' as p; | 
|  | 12 | +import 'package:test/test.dart'; | 
|  | 13 | + | 
|  | 14 | +import 'descriptor.dart'; | 
|  | 15 | +import 'directory_descriptor.dart'; | 
|  | 16 | +import 'file_descriptor.dart'; | 
|  | 17 | +import 'sandbox.dart'; | 
|  | 18 | +import 'utils.dart'; | 
|  | 19 | + | 
|  | 20 | +/// A [Descriptor] describing files in a Tar or Zip archive. | 
|  | 21 | +/// | 
|  | 22 | +/// The format is determined by the descriptor's file extension. | 
|  | 23 | +@sealed | 
|  | 24 | +class ArchiveDescriptor extends Descriptor implements FileDescriptor { | 
|  | 25 | +  /// Descriptors for entries in this archive. | 
|  | 26 | +  final List<Descriptor> contents; | 
|  | 27 | + | 
|  | 28 | +  /// Returns a `package:archive` [Archive] object that contains the contents of | 
|  | 29 | +  /// this file. | 
|  | 30 | +  Future<Archive> get archive async { | 
|  | 31 | +    var archive = Archive(); | 
|  | 32 | +    (await _files(contents)).forEach(archive.addFile); | 
|  | 33 | +    return archive; | 
|  | 34 | +  } | 
|  | 35 | + | 
|  | 36 | +  File get io => File(p.join(sandbox, name)); | 
|  | 37 | + | 
|  | 38 | +  /// Returns [ArchiveFile]s for each file in [descriptors]. | 
|  | 39 | +  /// | 
|  | 40 | +  /// If [parent] is passed, it's used as the parent directory for filenames. | 
|  | 41 | +  Future<Iterable<ArchiveFile>> _files(Iterable<Descriptor> descriptors, | 
|  | 42 | +      [String parent]) async { | 
|  | 43 | +    return (await waitAndReportErrors(descriptors.map((descriptor) async { | 
|  | 44 | +      var fullName = | 
|  | 45 | +          parent == null ? descriptor.name : "$parent/${descriptor.name}"; | 
|  | 46 | + | 
|  | 47 | +      if (descriptor is FileDescriptor) { | 
|  | 48 | +        var bytes = await collectBytes(descriptor.readAsBytes()); | 
|  | 49 | +        return [ | 
|  | 50 | +          ArchiveFile(fullName, bytes.length, bytes) | 
|  | 51 | +            // Setting the mode and mod time are necessary to work around | 
|  | 52 | +            // brendan-duncan/archive#76. | 
|  | 53 | +            ..mode = 428 | 
|  | 54 | +            ..lastModTime = DateTime.now().millisecondsSinceEpoch ~/ 1000 | 
|  | 55 | +        ]; | 
|  | 56 | +      } else if (descriptor is DirectoryDescriptor) { | 
|  | 57 | +        return await _files(descriptor.contents, fullName); | 
|  | 58 | +      } else { | 
|  | 59 | +        throw UnsupportedError( | 
|  | 60 | +            "An archive can only be created from FileDescriptors and " | 
|  | 61 | +            "DirectoryDescriptors."); | 
|  | 62 | +      } | 
|  | 63 | +    }))) | 
|  | 64 | +        .expand((files) => files); | 
|  | 65 | +  } | 
|  | 66 | + | 
|  | 67 | +  ArchiveDescriptor(String name, Iterable<Descriptor> contents) | 
|  | 68 | +      : contents = List.unmodifiable(contents), | 
|  | 69 | +        super(name); | 
|  | 70 | + | 
|  | 71 | +  Future create([String parent]) async { | 
|  | 72 | +    var path = p.join(parent ?? sandbox, name); | 
|  | 73 | +    var file = File(path).openWrite(); | 
|  | 74 | +    try { | 
|  | 75 | +      try { | 
|  | 76 | +        await readAsBytes().listen(file.add).asFuture(); | 
|  | 77 | +      } finally { | 
|  | 78 | +        await file.close(); | 
|  | 79 | +      } | 
|  | 80 | +    } catch (_) { | 
|  | 81 | +      await File(path).delete(); | 
|  | 82 | +      rethrow; | 
|  | 83 | +    } | 
|  | 84 | +  } | 
|  | 85 | + | 
|  | 86 | +  Future<String> read() async => throw UnsupportedError( | 
|  | 87 | +      "ArchiveDescriptor.read() is not supported. Use Archive.readAsBytes() " | 
|  | 88 | +      "instead."); | 
|  | 89 | + | 
|  | 90 | +  Stream<List<int>> readAsBytes() => Stream.fromFuture(() async { | 
|  | 91 | +        return _encodeFunction()(await archive); | 
|  | 92 | +      }()); | 
|  | 93 | + | 
|  | 94 | +  Future<void> validate([String parent]) async { | 
|  | 95 | +    // Access this first so we eaerly throw an error for a path with an invalid | 
|  | 96 | +    // extension. | 
|  | 97 | +    var decoder = _decodeFunction(); | 
|  | 98 | + | 
|  | 99 | +    var fullPath = p.join(parent ?? sandbox, name); | 
|  | 100 | +    var pretty = prettyPath(fullPath); | 
|  | 101 | +    if (!(await File(fullPath).exists())) { | 
|  | 102 | +      fail('File not found: "$pretty".'); | 
|  | 103 | +    } | 
|  | 104 | + | 
|  | 105 | +    var bytes = await File(fullPath).readAsBytes(); | 
|  | 106 | +    Archive archive; | 
|  | 107 | +    try { | 
|  | 108 | +      archive = decoder(bytes); | 
|  | 109 | +    } catch (_) { | 
|  | 110 | +      // Catch every error to work around brendan-duncan/archive#77. | 
|  | 111 | +      fail('File "$pretty" is not a valid archive.'); | 
|  | 112 | +    } | 
|  | 113 | + | 
|  | 114 | +    // Because validators expect to validate against a real filesystem, we have | 
|  | 115 | +    // to extract the archive to a temp directory and run validation on that. | 
|  | 116 | +    var tempDir = await Directory.systemTemp | 
|  | 117 | +        .createTempSync('dart_test_') | 
|  | 118 | +        .resolveSymbolicLinks(); | 
|  | 119 | + | 
|  | 120 | +    try { | 
|  | 121 | +      await waitAndReportErrors(archive.files.map((file) async { | 
|  | 122 | +        var path = p.join(tempDir, file.name); | 
|  | 123 | +        await Directory(p.dirname(path)).create(recursive: true); | 
|  | 124 | +        await File(path).writeAsBytes(file.content as List<int>); | 
|  | 125 | +      })); | 
|  | 126 | + | 
|  | 127 | +      await waitAndReportErrors(contents.map((entry) async { | 
|  | 128 | +        try { | 
|  | 129 | +          await entry.validate(tempDir); | 
|  | 130 | +        } on TestFailure catch (error) { | 
|  | 131 | +          // Replace the temporary directory with the path to the archive to | 
|  | 132 | +          // make the error more user-friendly. | 
|  | 133 | +          fail(error.message.replaceAll(tempDir, pretty)); | 
|  | 134 | +        } | 
|  | 135 | +      })); | 
|  | 136 | +    } finally { | 
|  | 137 | +      await Directory(tempDir).delete(recursive: true); | 
|  | 138 | +    } | 
|  | 139 | +  } | 
|  | 140 | + | 
|  | 141 | +  /// Returns the function to use to encode this file to binary, based on its | 
|  | 142 | +  /// [name]. | 
|  | 143 | +  List<int> Function(Archive) _encodeFunction() { | 
|  | 144 | +    if (name.endsWith(".zip")) { | 
|  | 145 | +      return ZipEncoder().encode; | 
|  | 146 | +    } else if (name.endsWith(".tar")) { | 
|  | 147 | +      return TarEncoder().encode; | 
|  | 148 | +    } else if (name.endsWith(".tar.gz") || | 
|  | 149 | +        name.endsWith(".tar.gzip") || | 
|  | 150 | +        name.endsWith(".tgz")) { | 
|  | 151 | +      return (archive) => GZipEncoder().encode(TarEncoder().encode(archive)); | 
|  | 152 | +    } else if (name.endsWith(".tar.bz2") || name.endsWith(".tar.bzip2")) { | 
|  | 153 | +      return (archive) => BZip2Encoder().encode(TarEncoder().encode(archive)); | 
|  | 154 | +    } else { | 
|  | 155 | +      throw UnsupportedError("Unknown file format $name."); | 
|  | 156 | +    } | 
|  | 157 | +  } | 
|  | 158 | + | 
|  | 159 | +  /// Returns the function to use to decode this file from binary, based on its | 
|  | 160 | +  /// [name]. | 
|  | 161 | +  Archive Function(List<int>) _decodeFunction() { | 
|  | 162 | +    if (name.endsWith(".zip")) { | 
|  | 163 | +      return ZipDecoder().decodeBytes; | 
|  | 164 | +    } else if (name.endsWith(".tar")) { | 
|  | 165 | +      return TarDecoder().decodeBytes; | 
|  | 166 | +    } else if (name.endsWith(".tar.gz") || | 
|  | 167 | +        name.endsWith(".tar.gzip") || | 
|  | 168 | +        name.endsWith(".tgz")) { | 
|  | 169 | +      return (archive) => | 
|  | 170 | +          TarDecoder().decodeBytes(GZipDecoder().decodeBytes(archive)); | 
|  | 171 | +    } else if (name.endsWith(".tar.bz2") || name.endsWith(".tar.bzip2")) { | 
|  | 172 | +      return (archive) => | 
|  | 173 | +          TarDecoder().decodeBytes(BZip2Decoder().decodeBytes(archive)); | 
|  | 174 | +    } else { | 
|  | 175 | +      throw UnsupportedError("Unknown file format $name."); | 
|  | 176 | +    } | 
|  | 177 | +  } | 
|  | 178 | + | 
|  | 179 | +  String describe() => describeDirectory(name, contents); | 
|  | 180 | +} | 
0 commit comments