Repository: brendan-duncan/archive

Issue:
fix: nested `zipDirectory` filename causes zipping corrupted zip
## Description

When using `zipDirectory` and specifying the `filename` as a path within the `directory`, the resulting zipped file will have a corrupted zip inside.

### Minimal reproductive sample code

[Repository with sample code](https://github.com/alestiago/issues/tree/archive-297-20231107)

```dart
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:path/path.dart' as path;

void main() {
  final inputDirectory = Directory(
    path.join(Directory.current.path, 'input_zip'),
  )..createSync(recursive: true);
  final outputDirectory = Directory(
    path.join(Directory.current.path, 'output_zip'),
  )..createSync(recursive: true);

  File(path.join(inputDirectory.path, 'file1.txt')).writeAsStringSync('file1');

  const zipFileName = 'output1.zip';
  final zipFilePath = path.join(inputDirectory.path, zipFileName);
  ZipFileEncoder().zipDirectory(inputDirectory, filename: zipFilePath);

  final zipBytes = File(
    zipFilePath,
  ).readAsBytesSync();

  File(
    path.join(outputDirectory.path, 'output2.zip'),
  )
    ..createSync(recursive: true)
    ..writeAsBytesSync(zipBytes);
}
```

Run the above snippet of code locally and unzip the output's directory zip file. It will have the text file and a corrupted zip.

### Proposals

- Show warning when `filename`'s path is nested within `directory.path`
- Avoiding `filename` being a path and only extract the filename from the `filename` path
- Gracefully detect and resolve this scenarios 

### Additional context

- I'm willing to work on a solution if we agree this is an issue and settle on how this should be resolved.


Discussion: I'm trying to figure out what you're doing here, but yes there does seem to be a problem. You're zipping a directory, and the path of the zip file being created is inside the directory you're zipping.

My thought is it should throw an exception in this case.
> My thought is it should throw an exception in this case.

I'm happy with this approach! Would you like me to contribute or do you prefer taking care of this?
I'd be glad if you could take care of it, thanks!
Fantastic! Feel free to assign this to me, I will look into making a Pull Request between this week and next week.

Fix patch:
diff --git a/lib/src/io/zip_file_encoder.dart b/lib/src/io/zip_file_encoder.dart
index 3b5bc200..00dd71aa 100644
--- a/lib/src/io/zip_file_encoder.dart
+++ b/lib/src/io/zip_file_encoder.dart
@@ -18,6 +18,14 @@ class ZipFileEncoder {
 
   ZipFileEncoder({this.password});
 
+  /// Zips a [dir] to a Zip file synchronously.
+  ///
+  /// {@macro ZipFileEncoder._composeZipDirectoryPath.filename}
+  ///
+  /// See also:
+  ///
+  /// * [zipDirectoryAsync] for the asynchronous version of this method.
+  /// * [_composeZipDirectoryPath] for the logic of composing the Zip file path.
   //@Deprecated('Use zipDirectoryAsync instead')
   void zipDirectory(Directory dir,
       {String? filename,
@@ -25,10 +33,12 @@ class ZipFileEncoder {
       bool followLinks = true,
       void Function(double)? onProgress,
       DateTime? modified}) {
-    final dirPath = dir.path;
-    final zipPath = filename ?? '$dirPath.zip';
-    level ??= GZIP;
-    create(zipPath, level: level, modified: modified);
+    create(
+      _composeZipDirectoryPath(dir: dir, filename: filename),
+      level: level ??= GZIP,
+      modified: modified,
+    );
+
     _addDirectory(
       dir,
       includeDirName: false,
@@ -39,16 +49,26 @@ class ZipFileEncoder {
     close();
   }
 
+  /// Zips a [dir] to a Zip file asynchronously.
+  ///
+  /// {@macro ZipFileEncoder._composeZipDirectoryPath.filename}
+  ///
+  /// See also:
+  ///
+  /// * [zipDirectory] for the synchronous version of this method.
+  /// * [_composeZipDirectoryPath] for the logic of composing the Zip file path.
   Future<void> zipDirectoryAsync(Directory dir,
       {String? filename,
       int? level,
       bool followLinks = true,
       void Function(double)? onProgress,
       DateTime? modified}) async {
-    final dirPath = dir.path;
-    final zipPath = filename ?? '$dirPath.zip';
-    level ??= GZIP;
-    create(zipPath, level: level, modified: modified);
+    create(
+      _composeZipDirectoryPath(dir: dir, filename: filename),
+      level: level ??= GZIP,
+      modified: modified,
+    );
+
     await addDirectory(dir,
         includeDirName: false,
         level: level,
@@ -57,6 +77,37 @@ class ZipFileEncoder {
     close();
   }
 
+  /// Composes the path (target) of the Zip file after a [Directory] is zipped.
+  ///
+  /// {@template ZipFileEncoder._composeZipDirectoryPath.filename}
+  /// [filename] determines where the Zip file will be created. If [filename]
+  /// is not specified, the name of the directory will be used with a '.zip'
+  /// extension. If [filename] is within [dir], it will throw a [FormatException].
+  /// {@endtemplate}
+  ///
+  /// See also:
+  ///
+  /// * [zipDirectory] and [zipDirectoryAsync] for the methods that use this logic.
+  String _composeZipDirectoryPath({
+    required Directory dir,
+    required String? filename,
+  }) {
+    final dirPath = dir.path;
+
+    if (filename == null) {
+      return '$dirPath.zip';
+    }
+
+    if (path.isWithin(dirPath, filename)) {
+      throw FormatException(
+        'filename must not be within the directory being zipped',
+        filename,
+      );
+    }
+
+    return filename;
+  }
+
   void open(String zipPath) => create(zipPath);
 
   void create(String zipPath, {int? level, DateTime? modified}) {


Test patch:
diff --git a/test/tests/io_test.dart b/test/tests/io_test.dart
index d1af5a18..1e8b27e5 100644
--- a/test/tests/io_test.dart
+++ b/test/tests/io_test.dart
@@ -472,4 +472,64 @@ void main() {
     expect(dstFiles.length, equals(srcFiles.length));
     //encoder.close();
   });
+
+  group('$ZipFileEncoder', () {
+    test(
+      'zipDirectory throws a FormatException when filename is within dir',
+      () {
+        final encoder = ZipFileEncoder();
+        final invalidFilename = p.join(testDirPath, 'res/test2.zip');
+
+        expect(
+          () => encoder.zipDirectory(
+            Directory(testDirPath),
+            filename: invalidFilename,
+          ),
+          throwsA(
+            isA<FormatException>()
+                .having(
+                  (exception) => exception.message,
+                  'message',
+                  equals(
+                      'filename must not be within the directory being zipped'),
+                )
+                .having(
+                  (exception) => exception.source,
+                  'source',
+                  equals(invalidFilename),
+                ),
+          ),
+        );
+      },
+    );
+
+    test(
+      'zipDirectoryAsync throws a FormatException when filename is within dir',
+      () async {
+        final encoder = ZipFileEncoder();
+        final invalidFilename = p.join(testDirPath, 'res/test2.zip');
+
+        await expectLater(
+          () => encoder.zipDirectoryAsync(
+            Directory(testDirPath),
+            filename: invalidFilename,
+          ),
+          throwsA(
+            isA<FormatException>()
+                .having(
+                  (exception) => exception.message,
+                  'message',
+                  equals(
+                      'filename must not be within the directory being zipped'),
+                )
+                .having(
+                  (exception) => exception.source,
+                  'source',
+                  equals(invalidFilename),
+                ),
+          ),
+        );
+      },
+    );
+  });
 }