Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: dart

dart:
- dev
- 2.0.0
- stable

dart_task:
- test
Expand All @@ -14,7 +14,7 @@ matrix:
- dart: dev
dart_task:
dartanalyzer: --fatal-infos --fatal-warnings .
- dart: 2.0.0
- dart: stable
dart_task:
dartanalyzer: --fatal-warnings .

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.2.0

* Add an `ArchiveDescriptor` class and a corresponding `archive()` function that
can create and validate Zip and TAR archives.

## 1.1.1

* Update to lowercase Dart core library constants.
Expand Down
180 changes: 180 additions & 0 deletions lib/src/archive_descriptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. 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:async';
import 'dart:io';

import 'package:archive/archive.dart';
import 'package:async/async.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';

import 'descriptor.dart';
import 'directory_descriptor.dart';
import 'file_descriptor.dart';
import 'sandbox.dart';
import 'utils.dart';

/// A [Descriptor] describing files in a Tar or Zip archive.
///
/// The format is determined by the descriptor's file extension.
@sealed
class ArchiveDescriptor extends Descriptor implements FileDescriptor {
/// Descriptors for entries in this archive.
final List<Descriptor> contents;

/// Returns a `package:archive` [Archive] object that contains the contents of
/// this file.
Future<Archive> get archive async {
var archive = Archive();
(await _files(contents)).forEach(archive.addFile);
return archive;
}

File get io => File(p.join(sandbox, name));

/// Returns [ArchiveFile]s for each file in [descriptors].
///
/// If [parent] is passed, it's used as the parent directory for filenames.
Future<Iterable<ArchiveFile>> _files(Iterable<Descriptor> descriptors,
[String parent]) async {
return (await waitAndReportErrors(descriptors.map((descriptor) async {
var fullName =
parent == null ? descriptor.name : "$parent/${descriptor.name}";

if (descriptor is FileDescriptor) {
var bytes = await collectBytes(descriptor.readAsBytes());
return [
ArchiveFile(fullName, bytes.length, bytes)
// Setting the mode and mod time are necessary to work around
// brendan-duncan/archive#76.
..mode = 428
..lastModTime = DateTime.now().millisecondsSinceEpoch ~/ 1000
];
} else if (descriptor is DirectoryDescriptor) {
return await _files(descriptor.contents, fullName);
} else {
throw UnsupportedError(
"An archive can only be created from FileDescriptors and "
"DirectoryDescriptors.");
}
})))
.expand((files) => files);
}

ArchiveDescriptor(String name, Iterable<Descriptor> contents)
: contents = List.unmodifiable(contents),
super(name);

Future create([String parent]) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be typed as Future<void>, for understandability?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was matching the superclass's signature. Probably the whole package should be converted to Future<void>, but I don't think this is the PR for it.

var path = p.join(parent ?? sandbox, name);
var file = File(path).openWrite();
try {
try {
await readAsBytes().listen(file.add).asFuture();
} finally {
await file.close();
}
} catch (_) {
await File(path).delete();
rethrow;
}
}

Future<String> read() async => throw UnsupportedError(
"ArchiveDescriptor.read() is not supported. Use Archive.readAsBytes() "
"instead.");

Stream<List<int>> readAsBytes() => Stream.fromFuture(() async {
return _encodeFunction()(await archive);
}());

Future<void> validate([String parent]) async {
// Access this first so we eaerly throw an error for a path with an invalid
// extension.
var decoder = _decodeFunction();

var fullPath = p.join(parent ?? sandbox, name);
var pretty = prettyPath(fullPath);
if (!(await File(fullPath).exists())) {
fail('File not found: "$pretty".');
}

var bytes = await File(fullPath).readAsBytes();
Archive archive;
try {
archive = decoder(bytes);
} catch (_) {
// Catch every error to work around brendan-duncan/archive#77.
fail('File "$pretty" is not a valid archive.');
}

// Because validators expect to validate against a real filesystem, we have
// to extract the archive to a temp directory and run validation on that.
var tempDir = await Directory.systemTemp
.createTempSync('dart_test_')
.resolveSymbolicLinks();

try {
await waitAndReportErrors(archive.files.map((file) async {
var path = p.join(tempDir, file.name);
await Directory(p.dirname(path)).create(recursive: true);
await File(path).writeAsBytes(file.content as List<int>);
}));

await waitAndReportErrors(contents.map((entry) async {
try {
await entry.validate(tempDir);
} on TestFailure catch (error) {
// Replace the temporary directory with the path to the archive to
// make the error more user-friendly.
fail(error.message.replaceAll(tempDir, pretty));
}
}));
} finally {
await Directory(tempDir).delete(recursive: true);
}
}

/// Returns the function to use to encode this file to binary, based on its
/// [name].
List<int> Function(Archive) _encodeFunction() {
if (name.endsWith(".zip")) {
return ZipEncoder().encode;
} else if (name.endsWith(".tar")) {
return TarEncoder().encode;
} else if (name.endsWith(".tar.gz") ||
name.endsWith(".tar.gzip") ||
name.endsWith(".tgz")) {
return (archive) => GZipEncoder().encode(TarEncoder().encode(archive));
} else if (name.endsWith(".tar.bz2") || name.endsWith(".tar.bzip2")) {
return (archive) => BZip2Encoder().encode(TarEncoder().encode(archive));
} else {
throw UnsupportedError("Unknown file format $name.");
}
}

/// Returns the function to use to decode this file from binary, based on its
/// [name].
Archive Function(List<int>) _decodeFunction() {
if (name.endsWith(".zip")) {
return ZipDecoder().decodeBytes;
} else if (name.endsWith(".tar")) {
return TarDecoder().decodeBytes;
} else if (name.endsWith(".tar.gz") ||
name.endsWith(".tar.gzip") ||
name.endsWith(".tgz")) {
return (archive) =>
TarDecoder().decodeBytes(GZipDecoder().decodeBytes(archive));
} else if (name.endsWith(".tar.bz2") || name.endsWith(".tar.bzip2")) {
return (archive) =>
TarDecoder().decodeBytes(BZip2Decoder().decodeBytes(archive));
} else {
throw UnsupportedError("Unknown file format $name.");
}
}

String describe() => describeDirectory(name, contents);
}
21 changes: 1 addition & 20 deletions lib/src/directory_descriptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'dart:io';

import 'package:async/async.dart';
import 'package:path/path.dart' as p;
import 'package:term_glyph/term_glyph.dart' as glyph;
import 'package:test/test.dart';

import 'descriptor.dart';
Expand Down Expand Up @@ -122,23 +121,5 @@ class DirectoryDescriptor extends Descriptor {
}));
}

String describe() {
if (contents.isEmpty) return name;

var buffer = StringBuffer();
buffer.writeln(name);
for (var entry in contents.take(contents.length - 1)) {
var entryString =
prefixLines(entry.describe(), '${glyph.verticalLine} ',
first: '${glyph.teeRight}${glyph.horizontalLine}'
'${glyph.horizontalLine} ');
buffer.writeln(entryString);
}

var lastEntryString = prefixLines(contents.last.describe(), ' ',
first: '${glyph.bottomLeftCorner}${glyph.horizontalLine}'
'${glyph.horizontalLine} ');
buffer.write(lastEntryString);
return buffer.toString();
}
String describe() => describeDirectory(name, contents);
}
24 changes: 23 additions & 1 deletion lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:path/path.dart' as p;
import 'package:term_glyph/term_glyph.dart' as glyph;
import 'package:test/test.dart';

import 'descriptor.dart';
import 'sandbox.dart';

/// A UTF-8 codec that allows malformed byte sequences.
Expand All @@ -25,6 +26,27 @@ String addBullet(String text) =>
/// Converts [strings] to a bulleted list.
String bullet(Iterable<String> strings) => strings.map(addBullet).join("\n");

/// Returns a human-readable description of a directory with the given [name]
/// and [contents].
String describeDirectory(String name, List<Descriptor> contents) {
if (contents.isEmpty) return name;

var buffer = StringBuffer();
buffer.writeln(name);
for (var entry in contents.take(contents.length - 1)) {
var entryString = prefixLines(entry.describe(), '${glyph.verticalLine} ',
first: '${glyph.teeRight}${glyph.horizontalLine}'
'${glyph.horizontalLine} ');
buffer.writeln(entryString);
}

var lastEntryString = prefixLines(contents.last.describe(), ' ',
first: '${glyph.bottomLeftCorner}${glyph.horizontalLine}'
'${glyph.horizontalLine} ');
buffer.write(lastEntryString);
return buffer.toString();
}

/// Prepends each line in [text] with [prefix].
///
/// If [first] or [last] is passed, the first and last lines, respectively, are
Expand Down Expand Up @@ -67,7 +89,7 @@ bool matchesAll(Pattern pattern, String string) =>

/// Like [Future.wait] with `eagerError: true`, but reports errors after the
/// first using [registerException] rather than silently ignoring them.
Future waitAndReportErrors(Iterable<Future> futures) {
Future<List<T>> waitAndReportErrors<T>(Iterable<Future<T>> futures) {
var errored = false;
return Future.wait(futures.map((future) {
// Avoid async/await so that we synchronously add error handlers for the
Expand Down
15 changes: 15 additions & 0 deletions lib/test_descriptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import 'package:path/path.dart' as p;
import 'package:test/test.dart';

import 'src/archive_descriptor.dart';
import 'src/descriptor.dart';
import 'src/directory_descriptor.dart';
import 'src/file_descriptor.dart';
import 'src/nothing_descriptor.dart';
import 'src/pattern_descriptor.dart';
import 'src/sandbox.dart';

export 'src/archive_descriptor.dart';
export 'src/descriptor.dart';
export 'src/directory_descriptor.dart';
export 'src/file_descriptor.dart';
Expand Down Expand Up @@ -72,5 +74,18 @@ PatternDescriptor filePattern(Pattern name, [contents]) =>
PatternDescriptor dirPattern(Pattern name, [Iterable<Descriptor> contents]) =>
pattern(name, (realName) => dir(realName, contents));

/// Creates a new [ArchiveDescriptor] with [name] and [contents].
///
/// [Descriptor.create] creates an archive with the given files and directories
/// within it, and [Descriptor.validate] validates that the archive contains the
/// given contents. It *doesn't* require that no other children exist. To ensure
/// that a particular child doesn't exist, use [nothing].
///
/// The type of the archive is determined by [name]'s file extension. It
/// supports `.zip`, `.tar`, `.tar.gz`/`.tar.gzip`/`.tgz`, and
/// `.tar.bz2`/`.tar.bzip2` files.
ArchiveDescriptor archive(String name, [Iterable<Descriptor> contents]) =>
ArchiveDescriptor(name, contents ?? []);

/// Returns [path] within the [sandbox] directory.
String path(String path) => p.join(sandbox, path);
8 changes: 5 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: test_descriptor
version: 1.1.1
version: 1.2.0
description: An API for defining and verifying directory structures.
author: Dart Team <[email protected]>
homepage: https://github.com/dart-lang/test_descriptor
Expand All @@ -8,12 +8,14 @@ environment:
sdk: '>=2.0.0 <3.0.0'

dependencies:
async: '>=1.10.0 <3.0.0'
archive: '^2.0.0'
async: '>=1.13.0 <3.0.0'
collection: '^1.5.0'
matcher: '^0.12.0'
meta: '^1.1.7'
path: '^1.0.0'
stack_trace: '^1.0.0'
test: '>=0.12.19 <2.0.0'
test: '^1.6.0'
term_glyph: '^1.0.0'

dev_dependencies:
Expand Down
Loading