blob: e206286546e1c930879558da38f9d5ec6398272a [file] [log] [blame]
// Copyright (c) 2012, 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:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:pub/src/exceptions.dart';
import 'package:pub/src/io.dart';
import 'package:tar/tar.dart';
import 'package:test/test.dart';
import 'descriptor.dart' as d;
import 'test_pub.dart';
void main() {
group('process', () {
final nonExisting = p.join(p.dirname(Platform.resolvedExecutable), 'gone');
test('Nice error message when failing to start process.', () {
final throwsNiceErrorMessage = throwsA(
predicate(
(e) =>
e is ApplicationException &&
e.message.contains(
'Pub failed to run subprocess `$nonExisting`: ProcessException:',
),
),
);
expect(
() => runProcess(nonExisting, ['a', 'b', 'c']),
throwsNiceErrorMessage,
);
expect(
() => startProcess(nonExisting, ['a', 'b', 'c']),
throwsNiceErrorMessage,
);
expect(
() => runProcessSync(nonExisting, ['a', 'b', 'c']),
throwsNiceErrorMessage,
);
});
});
group('listDir', () {
test('ignores hidden files by default', () {
expect(
withTempDir((temp) {
writeTextFile(p.join(temp, 'file1.txt'), '');
writeTextFile(p.join(temp, 'file2.txt'), '');
writeTextFile(p.join(temp, '.file3.txt'), '');
_createDir(p.join(temp, '.subdir'));
writeTextFile(p.join(temp, '.subdir', 'file3.txt'), '');
expect(
listDir(temp, recursive: true),
unorderedEquals(
[p.join(temp, 'file1.txt'), p.join(temp, 'file2.txt')],
),
);
}),
completes,
);
});
test('includes hidden files when told to', () {
expect(
withTempDir((temp) {
writeTextFile(p.join(temp, 'file1.txt'), '');
writeTextFile(p.join(temp, 'file2.txt'), '');
writeTextFile(p.join(temp, '.file3.txt'), '');
_createDir(p.join(temp, '.subdir'));
writeTextFile(p.join(temp, '.subdir', 'file3.txt'), '');
expect(
listDir(temp, recursive: true, includeHidden: true),
unorderedEquals([
p.join(temp, 'file1.txt'),
p.join(temp, 'file2.txt'),
p.join(temp, '.file3.txt'),
p.join(temp, '.subdir'),
p.join(temp, '.subdir', 'file3.txt'),
]),
);
}),
completes,
);
});
test("doesn't ignore hidden files above the directory being listed", () {
expect(
withTempDir((temp) {
var dir = p.join(temp, '.foo', 'bar');
ensureDir(dir);
writeTextFile(p.join(dir, 'file1.txt'), '');
writeTextFile(p.join(dir, 'file2.txt'), '');
writeTextFile(p.join(dir, 'file3.txt'), '');
expect(
listDir(dir, recursive: true),
unorderedEquals([
p.join(dir, 'file1.txt'),
p.join(dir, 'file2.txt'),
p.join(dir, 'file3.txt'),
]),
);
}),
completes,
);
});
});
group('canonicalize', () {
test('resolves a non-link', () {
expect(
_withCanonicalTempDir((temp) {
var filePath = p.join(temp, 'file');
writeTextFile(filePath, '');
expect(canonicalize(filePath), equals(filePath));
}),
completes,
);
});
test('resolves a non-existent file', () {
expect(
_withCanonicalTempDir((temp) {
expect(
canonicalize(p.join(temp, 'nothing')),
equals(p.join(temp, 'nothing')),
);
}),
completes,
);
});
test('resolves a symlink', () {
expect(
_withCanonicalTempDir((temp) {
_createDir(p.join(temp, 'linked-dir'));
createSymlink(p.join(temp, 'linked-dir'), p.join(temp, 'dir'));
expect(
canonicalize(p.join(temp, 'dir')),
equals(p.join(temp, 'linked-dir')),
);
}),
completes,
);
});
test('resolves a symlink to parent', () {
expect(
_withCanonicalTempDir((temp) {
_createDir(p.join(temp, 'linked-dir'));
_createDir(p.join(temp, 'linked-dir', 'a'));
_createDir(p.join(temp, 'linked-dir', 'b'));
createSymlink(
p.join(temp, 'linked-dir'),
p.join(temp, 'linked-dir', 'a', 'symlink'),
);
expect(
canonicalize(p.join(temp, 'linked-dir', 'a', 'symlink', 'b')),
equals(p.join(temp, 'linked-dir', 'b')),
);
}),
completes,
);
});
test('resolves a relative symlink', () {
expect(
_withCanonicalTempDir((temp) {
_createDir(p.join(temp, 'linked-dir'));
createSymlink(
p.join(temp, 'linked-dir'),
p.join(temp, 'dir'),
relative: true,
);
expect(
canonicalize(p.join(temp, 'dir')),
equals(p.join(temp, 'linked-dir')),
);
}),
completes,
);
});
test('resolves a single-level horizontally recursive symlink', () {
expect(
_withCanonicalTempDir((temp) {
var linkPath = p.join(temp, 'foo');
createSymlink(linkPath, linkPath);
expect(canonicalize(linkPath), equals(linkPath));
}),
completes,
);
});
test('resolves a multi-level horizontally recursive symlink', () {
expect(
_withCanonicalTempDir((temp) {
var fooPath = p.join(temp, 'foo');
var barPath = p.join(temp, 'bar');
var bazPath = p.join(temp, 'baz');
createSymlink(barPath, fooPath);
createSymlink(bazPath, barPath);
createSymlink(fooPath, bazPath);
expect(canonicalize(fooPath), equals(fooPath));
expect(canonicalize(barPath), equals(barPath));
expect(canonicalize(bazPath), equals(bazPath));
createSymlink(fooPath, p.join(temp, 'outer'));
expect(canonicalize(p.join(temp, 'outer')), equals(fooPath));
}),
completes,
);
});
test('resolves a broken symlink', () {
expect(
_withCanonicalTempDir((temp) {
createSymlink(p.join(temp, 'nonexistent'), p.join(temp, 'foo'));
expect(
canonicalize(p.join(temp, 'foo')),
equals(p.join(temp, 'nonexistent')),
);
}),
completes,
);
});
test('resolves multiple nested symlinks', () {
expect(
_withCanonicalTempDir((temp) {
var dir1 = p.join(temp, 'dir1');
var dir2 = p.join(temp, 'dir2');
var subdir1 = p.join(dir1, 'subdir1');
var subdir2 = p.join(dir2, 'subdir2');
_createDir(dir2);
_createDir(subdir2);
createSymlink(dir2, dir1);
createSymlink(subdir2, subdir1);
expect(
canonicalize(p.join(subdir1, 'file')),
equals(p.join(subdir2, 'file')),
);
}),
completes,
);
});
test('resolves a nested vertical symlink', () {
expect(
_withCanonicalTempDir((temp) {
var dir1 = p.join(temp, 'dir1');
var dir2 = p.join(temp, 'dir2');
var subdir = p.join(dir1, 'subdir');
_createDir(dir1);
_createDir(dir2);
createSymlink(dir2, subdir);
expect(
canonicalize(p.join(subdir, 'file')),
equals(p.join(dir2, 'file')),
);
}),
completes,
);
});
test('resolves a vertically recursive symlink', () {
expect(
_withCanonicalTempDir((temp) {
var dir = p.join(temp, 'dir');
var subdir = p.join(dir, 'subdir');
_createDir(dir);
createSymlink(dir, subdir);
expect(
canonicalize(
p.join(
temp,
'dir',
'subdir',
'subdir',
'subdir',
'subdir',
'file',
),
),
equals(p.join(dir, 'file')),
);
}),
completes,
);
});
test('resolves a symlink that links to a path that needs more resolving',
() {
expect(
_withCanonicalTempDir((temp) {
var dir = p.join(temp, 'dir');
var linkdir = p.join(temp, 'linkdir');
var linkfile = p.join(dir, 'link');
_createDir(dir);
createSymlink(dir, linkdir);
createSymlink(p.join(linkdir, 'file'), linkfile);
expect(canonicalize(linkfile), equals(p.join(dir, 'file')));
}),
completes,
);
});
test('resolves a pair of pathologically-recursive symlinks', () {
expect(
_withCanonicalTempDir((temp) {
var foo = p.join(temp, 'foo');
var subfoo = p.join(foo, 'subfoo');
var bar = p.join(temp, 'bar');
var subbar = p.join(bar, 'subbar');
createSymlink(subbar, foo);
createSymlink(subfoo, bar);
expect(
canonicalize(subfoo),
equals(p.join(subfoo, 'subbar', 'subfoo')),
);
}),
completes,
);
});
});
group('extractTarGz', () {
test('decompresses simple archive', () async {
await withTempDir((tempDir) async {
await extractTarGz(
Stream.fromIterable(
[
base64Decode(
'H4sIAP2weF4AA+3S0QqCMBiG4V2KeAE1nfuF7maViNBqzDyQ6N4z6yCIogOtg97ncAz2wTvfuxCW'
'alZ6UFqttIiUYpXObWlzM57fqcyIkcxoU2ZKZyYvtErsvLNuuvboYpKotqm7uPUv74XYeBf7Oh66'
'8I1dX+LH/qFbt6HaLHrnd9O/cQ0sxZv++UP/Qob+1srQX08/5dmf9z+le+erdJWOHyE9/3oPAAAA'
'AAAAAAAAAAAAgM9dALkoaRMAKAAA'),
],
),
tempDir,
);
await d.dir(appPath, [
d.rawPubspec({
'name': 'myapp',
}),
]).validate(tempDir);
});
});
test('throws on tar error', () async {
await withTempDir((tempDir) async {
await expectLater(
() async => await extractTarGz(
Stream.fromIterable(
[
base64Decode(
// Correct Gzip of a faulty tar archive.
'H4sICBKyeF4AA215YXBwLnRhcgDt0sEKgjAAh/GdewrxAWpzbkJvs0pEaDVmHiR699Q6BBJ00Dr0'
'/Y5jsD98850LYSMWJXuFkUJaITNTmEyPR09Caaut0lIXSkils1yKxCy76KFtLi4miWjqqo0H//Ze'
'iLV3saviuQ3f2PUlfkwf2l0Tyv26c/44/xtDYJsP6a0trJn2z1765/3/UMbYvr+cf8rUn/e/pifn'
'y3Sbjh8hvf16DwAAAAAAAAAAAAAAAIDPre4CU/3q/CcAAA=='),
],
),
tempDir,
),
throwsA(isA<TarException>()),
);
});
});
test('throws on gzip error', () async {
await withTempDir((tempDir) async {
await expectLater(
() async => await extractTarGz(
Stream.fromIterable(
[
[10, 20, 30], // Not a good gz stream.
],
),
tempDir,
),
throwsA(
isA<FormatException>().having(
(e) => e.message,
'message',
contains('Filter error, bad data'),
),
),
);
});
});
test(
'applies executable bits from tar file',
() => withTempDir((tempDir) async {
final entry = Stream<TarEntry>.value(
TarEntry.data(
TarHeader(
name: 'weird_exe',
typeFlag: TypeFlag.reg,
mode: int.parse('110', radix: 8),
),
const [],
),
);
await extractTarGz(
entry.transform(tarWriter).transform(gzip.encoder),
tempDir,
);
expect(File('$tempDir/weird_exe').statSync().modeString(), 'rwxr-xr--');
}),
testOn: 'linux || mac-os',
);
test('extracts files and links', () {
return withTempDir((tempDir) async {
final entries = Stream<TarEntry>.fromIterable([
TarEntry.data(
TarHeader(name: 'lib/main.txt', typeFlag: TypeFlag.reg),
utf8.encode('text content'),
),
TarEntry.data(
TarHeader(
name: 'bin/main.txt',
typeFlag: TypeFlag.symlink,
linkName: '../lib/main.txt',
),
const [],
),
TarEntry.data(
TarHeader(
name: 'test/main.txt',
typeFlag: TypeFlag.link,
// TypeFlag.link is resolved against the root of the tar file
linkName: 'lib/main.txt',
),
const [],
),
]);
await extractTarGz(
entries.transform(tarWriter).transform(gzip.encoder),
tempDir,
);
await d.dir(
'.',
[
d.file('lib/main.txt', 'text content'),
d.file('bin/main.txt', 'text content'),
d.file('test/main.txt', 'text content'),
],
).validate(tempDir);
});
});
test('preserves empty directories', () {
return withTempDir((tempDir) async {
final entry = Stream<TarEntry>.value(
TarEntry.data(
TarHeader(
name: 'bin/',
typeFlag: TypeFlag.dir,
),
const [],
),
);
await extractTarGz(
entry.transform(tarWriter).transform(gzip.encoder),
tempDir,
);
await expectLater(
Directory(tempDir).list(),
emits(
isA<Directory>()
.having((e) => p.basename(e.path), 'basename', 'bin'),
),
);
});
});
test('throws for entries escaping the tar file', () {
return withTempDir((tempDir) async {
final entry = Stream<TarEntry>.value(
TarEntry.data(
TarHeader(
name: '../other_package-1.2.3/lib/file.dart',
typeFlag: TypeFlag.reg,
),
const [],
),
);
await expectLater(
extractTarGz(
entry.transform(tarWriter).transform(gzip.encoder),
tempDir,
),
throwsA(isA<FormatException>()),
);
await expectLater(Directory(tempDir).list(), emitsDone);
});
});
test('skips symlinks escaping the tar file', () {
return withTempDir((tempDir) async {
final entry = Stream<TarEntry>.value(
TarEntry.data(
TarHeader(
name: 'nested/bad_link',
typeFlag: TypeFlag.symlink,
linkName: '../../outside.txt',
),
const [],
),
);
await extractTarGz(
entry.transform(tarWriter).transform(gzip.encoder),
tempDir,
);
await expectLater(Directory(tempDir).list(), emitsDone);
});
});
test('skips hardlinks escaping the tar file', () {
return withTempDir((tempDir) async {
final entry = Stream<TarEntry>.value(
TarEntry.data(
TarHeader(
name: 'nested/bad_link',
typeFlag: TypeFlag.link,
linkName: '../outside.txt',
),
const [],
),
);
await extractTarGz(
entry.transform(tarWriter).transform(gzip.encoder),
tempDir,
);
await expectLater(Directory(tempDir).list(), emitsDone);
});
});
});
testExistencePredicate(
'entryExists',
entryExists,
forFile: true,
forFileSymlink: true,
forMultiLevelFileSymlink: true,
forDirectory: true,
forDirectorySymlink: true,
forMultiLevelDirectorySymlink: true,
forBrokenSymlink: true,
forMultiLevelBrokenSymlink: true,
);
testExistencePredicate(
'linkExists',
linkExists,
forFile: false,
forFileSymlink: true,
forMultiLevelFileSymlink: true,
forDirectory: false,
forDirectorySymlink: true,
forMultiLevelDirectorySymlink: true,
forBrokenSymlink: true,
forMultiLevelBrokenSymlink: true,
);
testExistencePredicate(
'fileExists',
fileExists,
forFile: true,
forFileSymlink: true,
forMultiLevelFileSymlink: true,
forDirectory: false,
forDirectorySymlink: false,
forMultiLevelDirectorySymlink: false,
forBrokenSymlink: false,
forMultiLevelBrokenSymlink: false,
);
testExistencePredicate(
'dirExists',
dirExists,
forFile: false,
forFileSymlink: false,
forMultiLevelFileSymlink: false,
forDirectory: true,
forDirectorySymlink: true,
forMultiLevelDirectorySymlink: true,
forBrokenSymlink: false,
forMultiLevelBrokenSymlink: false,
);
}
void testExistencePredicate(
String name,
bool Function(String path) predicate, {
required bool forFile,
required bool forFileSymlink,
required bool forMultiLevelFileSymlink,
required bool forDirectory,
required bool forDirectorySymlink,
required bool forMultiLevelDirectorySymlink,
required bool forBrokenSymlink,
required bool forMultiLevelBrokenSymlink,
}) {
group(name, () {
test('returns $forFile for a file', () {
expect(
withTempDir((temp) {
var file = p.join(temp, 'test.txt');
writeTextFile(file, 'contents');
expect(predicate(file), equals(forFile));
}),
completes,
);
});
test('returns $forDirectory for a directory', () {
expect(
withTempDir((temp) {
var file = p.join(temp, 'dir');
_createDir(file);
expect(predicate(file), equals(forDirectory));
}),
completes,
);
});
test('returns $forDirectorySymlink for a symlink to a directory', () {
expect(
withTempDir((temp) {
var targetPath = p.join(temp, 'dir');
var symlinkPath = p.join(temp, 'linkdir');
_createDir(targetPath);
createSymlink(targetPath, symlinkPath);
expect(predicate(symlinkPath), equals(forDirectorySymlink));
}),
completes,
);
});
test(
'returns $forMultiLevelDirectorySymlink for a multi-level symlink to '
'a directory', () {
expect(
withTempDir((temp) {
var targetPath = p.join(temp, 'dir');
var symlink1Path = p.join(temp, 'link1dir');
var symlink2Path = p.join(temp, 'link2dir');
_createDir(targetPath);
createSymlink(targetPath, symlink1Path);
createSymlink(symlink1Path, symlink2Path);
expect(
predicate(symlink2Path),
equals(forMultiLevelDirectorySymlink),
);
}),
completes,
);
});
test('returns $forBrokenSymlink for a broken symlink', () {
expect(
withTempDir((temp) {
var targetPath = p.join(temp, 'dir');
var symlinkPath = p.join(temp, 'linkdir');
_createDir(targetPath);
createSymlink(targetPath, symlinkPath);
deleteEntry(targetPath);
expect(predicate(symlinkPath), equals(forBrokenSymlink));
}),
completes,
);
});
test('returns $forMultiLevelBrokenSymlink for a multi-level broken symlink',
() {
expect(
withTempDir((temp) {
var targetPath = p.join(temp, 'dir');
var symlink1Path = p.join(temp, 'link1dir');
var symlink2Path = p.join(temp, 'link2dir');
_createDir(targetPath);
createSymlink(targetPath, symlink1Path);
createSymlink(symlink1Path, symlink2Path);
deleteEntry(targetPath);
expect(predicate(symlink2Path), equals(forMultiLevelBrokenSymlink));
}),
completes,
);
});
// Windows doesn't support symlinking to files.
if (!Platform.isWindows) {
test('returns $forFileSymlink for a symlink to a file', () {
expect(
withTempDir((temp) {
var targetPath = p.join(temp, 'test.txt');
var symlinkPath = p.join(temp, 'link.txt');
writeTextFile(targetPath, 'contents');
createSymlink(targetPath, symlinkPath);
expect(predicate(symlinkPath), equals(forFileSymlink));
}),
completes,
);
});
test(
'returns $forMultiLevelFileSymlink for a multi-level symlink to a '
'file', () {
expect(
withTempDir((temp) {
var targetPath = p.join(temp, 'test.txt');
var symlink1Path = p.join(temp, 'link1.txt');
var symlink2Path = p.join(temp, 'link2.txt');
writeTextFile(targetPath, 'contents');
createSymlink(targetPath, symlink1Path);
createSymlink(symlink1Path, symlink2Path);
expect(predicate(symlink2Path), equals(forMultiLevelFileSymlink));
}),
completes,
);
});
}
});
test('escapeShellArgument', () {
expect(escapeShellArgument(r'abc'), r'abc');
expect(escapeShellArgument(r'ab c'), r"'ab c'");
expect(escapeShellArgument(r'ab\c'), r"'ab\\c'");
expect(escapeShellArgument(r"ab\'c"), r"'ab\\'\''c'");
});
}
/// Like [withTempDir], but canonicalizes the path before passing it to [fn].
Future<T> _withCanonicalTempDir<T>(FutureOr<T> Function(String path) fn) =>
withTempDir((temp) => fn(canonicalize(temp)));
/// Creates a directory [dir].
String _createDir(String dir) {
Directory(dir).createSync();
return dir;
}