Normalize path before writing file when extracting tar.gz (#4752)

diff --git a/lib/src/io.dart b/lib/src/io.dart
index 04e3b05..8c85ca1 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -1159,11 +1159,13 @@
   while (await reader.moveNext()) {
     final entry = reader.current;
 
-    final filePath = p.joinAll([
-      destination,
-      // Tar file names always use forward slashes
-      ...p.posix.split(entry.name),
-    ]);
+    final filePath = p.normalize(
+      p.joinAll([
+        destination,
+        // Tar file names always use forward slashes
+        ...p.posix.split(entry.name),
+      ]),
+    );
     if (!paths.add(filePath)) {
       // The tar file contained the same entry twice. Assume it is broken.
       await reader.cancel();
diff --git a/test/io_test.dart b/test/io_test.dart
index bc6275a..7551763 100644
--- a/test/io_test.dart
+++ b/test/io_test.dart
@@ -552,6 +552,41 @@
       });
     });
 
+    test('avoid zip slip using combined symlink and ../', () {
+      return withTempDir((tempDir) async {
+        final entry = Stream<TarEntry>.fromIterable([
+          TarEntry.data(
+            TarHeader(
+              name: 'nested/bad_link',
+              typeFlag: TypeFlag.symlink,
+              linkName: '../nested',
+              mode: _defaultMode,
+            ),
+            const [],
+          ),
+          TarEntry.data(
+            TarHeader(
+              name: 'nested/bad_link/../../payload.txt',
+              typeFlag: TypeFlag.reg,
+              mode: _defaultMode,
+            ),
+            utf8.encode('text content'),
+          ),
+        ]);
+
+        await extractTarGz(
+          entry.transform(tarWriter).transform(gzip.encoder),
+          tempDir,
+        );
+        // Make sure that the payload did not slip outside the destination via
+        // the symlink.
+        expect(
+          Directory(tempDir).listSync().map((x) => x.path),
+          contains(endsWith('payload.txt')),
+        );
+      });
+    });
+
     test('skips hardlinks escaping the tar file', () {
       return withTempDir((tempDir) async {
         final entry = Stream<TarEntry>.value(