Fix handling of directories in archives (#4306)
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 730962b..521632f 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -1072,12 +1072,14 @@
throw FormatException('Tar file contained duplicate path ${entry.name}');
}
- if (!p.isWithin(destination, filePath)) {
+ if (!(p.isWithin(destination, filePath) ||
+ // allow including '.' as an entry in the tar.gz archive.
+ (entry.type == TypeFlag.dir && p.equals(destination, filePath)))) {
// The tar contains entries that would be written outside of the
// destination. That doesn't happen by accident, assume that the tar file
// is malicious.
await reader.cancel();
- throw FormatException('Invalid tar entry: ${entry.name}');
+ throw FormatException('Invalid tar entry: `${entry.name}`');
}
final parentDirectory = p.dirname(filePath);
@@ -1171,7 +1173,10 @@
final tarContents = Stream.fromIterable(
contents.map((entry) {
entry = p.normalize(p.absolute(entry));
- if (!p.equals(baseDir, entry) && !p.isWithin(baseDir, entry)) {
+ if (p.equals(baseDir, entry)) {
+ return null;
+ }
+ if (!p.isWithin(baseDir, entry)) {
throw ArgumentError('Entry $entry is not inside $baseDir.');
}
@@ -1189,7 +1194,13 @@
}
if (stat.type == FileSystemEntityType.directory) {
return TarEntry(
- TarHeader(name: name, typeFlag: TypeFlag.dir),
+ TarHeader(
+ name: name,
+ mode: _defaultMode | _executableMask,
+ typeFlag: TypeFlag.dir,
+ userName: 'pub',
+ groupName: 'pub',
+ ),
Stream.fromIterable([]),
);
} else {
@@ -1207,7 +1218,7 @@
file.openRead(),
);
}
- }),
+ }).whereNotNull(),
);
return ByteStream(
diff --git a/test/lish/empy_directories_test.dart b/test/lish/archive_contents_test.dart
similarity index 67%
rename from test/lish/empy_directories_test.dart
rename to test/lish/archive_contents_test.dart
index 0c24f50..40afd12 100644
--- a/test/lish/empy_directories_test.dart
+++ b/test/lish/archive_contents_test.dart
@@ -15,13 +15,29 @@
import '../test_pub.dart';
import 'utils.dart';
+/// The assumed default file mode on Linux and macOS
+const _defaultMode = 420; // 644₈
+
+/// Mask for executable bits in file modes.
+const _executableMask = 0x49; // 001 001 001
+
void main() {
- test('archives and uploads empty directories in package', () async {
+ test(
+ 'archives and uploads empty directories in package. Maintains the executable bit',
+ () async {
await d.validPackage().create();
await d.dir(appPath, [
+ d.dir('tool', [d.file('tool.sh', 'commands...')]),
d.dir('lib', [d.dir('empty')]),
]).create();
+ if (!Platform.isWindows) {
+ Process.runSync(
+ 'chmod',
+ ['+x', p.join(d.sandbox, appPath, 'tool', 'tool.sh')],
+ );
+ }
+
await servePackages();
await runPub(
args: ['publish', '--to-archive=archive.tar.gz'],
@@ -32,7 +48,9 @@
├── lib
│ ├── empty
│ └── test_pkg.dart (<1 KB)
-└── pubspec.yaml (<1 KB)
+├── pubspec.yaml (<1 KB)
+└── tool
+ └── tool.sh (<1 KB)
'''),
);
expect(
@@ -48,10 +66,27 @@
while (await tarReader.moveNext()) {
final entry = tarReader.current;
if (entry.type == TypeFlag.dir) {
+ if (!Platform.isWindows) {
+ expect(entry.header.mode, _defaultMode | _executableMask);
+ }
dirs.add(entry.name);
+ } else {
+ if (!Platform.isWindows) {
+ if (entry.name.endsWith('tool.sh')) {
+ expect(
+ entry.header.mode
+ // chmod +x doesn't sets the executable bit for other users on some platforms only.
+ |
+ 1,
+ _defaultMode | _executableMask,
+ );
+ } else {
+ expect(entry.header.mode, _defaultMode);
+ }
+ }
}
}
- expect(dirs, ['.', 'lib', 'lib/empty']);
+ expect(dirs, ['tool', 'lib', 'lib/empty']);
await d.credentialsFile(globalServer, 'access-token').create();
final pub = await startPublish(globalServer);
diff --git a/test/lish/publishing_to_and_from_archive_test.dart b/test/lish/publishing_to_and_from_archive_test.dart
index 9e2e6bf..1a6e72a 100644
--- a/test/lish/publishing_to_and_from_archive_test.dart
+++ b/test/lish/publishing_to_and_from_archive_test.dart
@@ -55,4 +55,17 @@
);
await pub.shouldExit(SUCCESS);
});
+
+ test('Can extract self-published archive', () async {
+ await d.validPackage().create();
+
+ await runPub(
+ args: ['lish', '--to-archive', p.join('..', 'archive.tar.gz')],
+ output: contains(
+ 'Wrote package archive at ${p.join('..', 'archive.tar.gz')}',
+ ),
+ );
+ expect(File(d.path('archive.tar.gz')).existsSync(), isTrue);
+ await runPub(args: ['cache', 'preload', p.join('..', 'archive.tar.gz')]);
+ });
}