diff --git a/src/SharpCompress/Common/Entry.cs b/src/SharpCompress/Common/Entry.cs index b5f58055..c12cdf6f 100644 --- a/src/SharpCompress/Common/Entry.cs +++ b/src/SharpCompress/Common/Entry.cs @@ -15,6 +15,11 @@ namespace SharpCompress.Common /// public abstract string Key { get; } + /// + /// The target of a symlink entry internal to the Archive. Will be null if not a symlink. + /// + public abstract string LinkTarget { get; } + /// /// The compressed file size /// diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index 71e67a97..15efd220 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -68,19 +68,30 @@ namespace SharpCompress.Common ExtractionOptions options, Action openAndWrite) { - FileMode fm = FileMode.Create; - options = options ?? new ExtractionOptions() - { - Overwrite = true - }; - - if (!options.Overwrite) + if (entry.LinkTarget != null) { - fm = FileMode.CreateNew; + if (null == options.WriteSymbolicLink) + { + throw new ExtractionException("Entry is a symbolic link but ExtractionOptions.WriteSymbolicLink delegate is null"); + } + options.WriteSymbolicLink(destinationFileName, entry.LinkTarget); } + else + { + FileMode fm = FileMode.Create; + options = options ?? new ExtractionOptions() + { + Overwrite = true + }; - openAndWrite(destinationFileName, fm); - entry.PreserveExtractionOptions(destinationFileName, options); + if (!options.Overwrite) + { + fm = FileMode.CreateNew; + } + + openAndWrite(destinationFileName, fm); + entry.PreserveExtractionOptions(destinationFileName, options); + } } #endif } diff --git a/src/SharpCompress/Common/ExtractionOptions.cs b/src/SharpCompress/Common/ExtractionOptions.cs index 9dba482e..7f6e1efc 100644 --- a/src/SharpCompress/Common/ExtractionOptions.cs +++ b/src/SharpCompress/Common/ExtractionOptions.cs @@ -21,5 +21,14 @@ /// preserve windows file attributes /// public bool PreserveAttributes { get; set; } + + /// + /// Delegate for writing symbolic links to disk. + /// sourcePath is where the symlink is created. + /// targetPath is what the symlink refers to. + /// + public delegate void SymbolicLinkWriterDelegate(string sourcePath, string targetPath); + + public SymbolicLinkWriterDelegate WriteSymbolicLink; } } \ No newline at end of file diff --git a/src/SharpCompress/Common/GZip/GZipEntry.cs b/src/SharpCompress/Common/GZip/GZipEntry.cs index 0b43cd45..8b2d3e93 100644 --- a/src/SharpCompress/Common/GZip/GZipEntry.cs +++ b/src/SharpCompress/Common/GZip/GZipEntry.cs @@ -20,6 +20,8 @@ namespace SharpCompress.Common.GZip public override string Key => _filePart.FilePartName; + public override string LinkTarget => null; + public override long CompressedSize => 0; public override long Size => 0; diff --git a/src/SharpCompress/Common/IEntry.cs b/src/SharpCompress/Common/IEntry.cs index e8f13a30..84cfcb33 100644 --- a/src/SharpCompress/Common/IEntry.cs +++ b/src/SharpCompress/Common/IEntry.cs @@ -10,6 +10,7 @@ namespace SharpCompress.Common long Crc { get; } DateTime? CreatedTime { get; } string Key { get; } + string LinkTarget { get; } bool IsDirectory { get; } bool IsEncrypted { get; } bool IsSplitAfter { get; } diff --git a/src/SharpCompress/Common/Rar/RarEntry.cs b/src/SharpCompress/Common/Rar/RarEntry.cs index 85be0bb3..c461f42a 100644 --- a/src/SharpCompress/Common/Rar/RarEntry.cs +++ b/src/SharpCompress/Common/Rar/RarEntry.cs @@ -22,6 +22,8 @@ namespace SharpCompress.Common.Rar /// public override string Key => FileHeader.FileName; + public override string LinkTarget => null; + /// /// The entry last modified time in the archive, if recorded /// diff --git a/src/SharpCompress/Common/SevenZip/SevenZipEntry.cs b/src/SharpCompress/Common/SevenZip/SevenZipEntry.cs index 584bfb95..c2ea2efe 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipEntry.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipEntry.cs @@ -18,6 +18,8 @@ namespace SharpCompress.Common.SevenZip public override string Key => FilePart.Header.Name; + public override string LinkTarget => null; + public override long CompressedSize => 0; public override long Size => FilePart.Header.Size; diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs index 7cdfcef3..db6d5cf4 100644 --- a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs +++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs @@ -15,6 +15,7 @@ namespace SharpCompress.Common.Tar.Headers } internal string Name { get; set; } + internal string LinkName { get; set; } //internal int Mode { get; set; } //internal int UserId { get; set; } @@ -98,6 +99,12 @@ namespace SharpCompress.Common.Tar.Headers return false; } + // for symlinks, additionally read the linkname + if (ReadEntryType(buffer) == EntryType.SymLink) + { + LinkName = ArchiveEncoding.Decode(buffer, 157, 100).TrimNulls(); + } + if (ReadEntryType(buffer) == EntryType.LongName) { Name = ReadLongName(reader, buffer); diff --git a/src/SharpCompress/Common/Tar/TarEntry.cs b/src/SharpCompress/Common/Tar/TarEntry.cs index 82e707d0..6ec5d319 100644 --- a/src/SharpCompress/Common/Tar/TarEntry.cs +++ b/src/SharpCompress/Common/Tar/TarEntry.cs @@ -23,6 +23,8 @@ namespace SharpCompress.Common.Tar public override string Key => _filePart.Header.Name; + public override string LinkTarget => _filePart.Header.LinkName; + public override long CompressedSize => _filePart.Header.Size; public override long Size => _filePart.Header.Size; diff --git a/src/SharpCompress/Common/Zip/ZipEntry.cs b/src/SharpCompress/Common/Zip/ZipEntry.cs index 379534ff..3f65adae 100644 --- a/src/SharpCompress/Common/Zip/ZipEntry.cs +++ b/src/SharpCompress/Common/Zip/ZipEntry.cs @@ -60,6 +60,8 @@ namespace SharpCompress.Common.Zip public override string Key => _filePart.Header.Name; + public override string LinkTarget => null; + public override long CompressedSize => _filePart.Header.CompressedSize; public override long Size => _filePart.Header.UncompressedSize; diff --git a/tests/SharpCompress.Test/SharpCompress.Test.csproj b/tests/SharpCompress.Test/SharpCompress.Test.csproj index 6223a62d..4d08ac18 100644 --- a/tests/SharpCompress.Test/SharpCompress.Test.csproj +++ b/tests/SharpCompress.Test/SharpCompress.Test.csproj @@ -16,5 +16,6 @@ + \ No newline at end of file diff --git a/tests/SharpCompress.Test/Tar/TarReaderTests.cs b/tests/SharpCompress.Test/Tar/TarReaderTests.cs index 10c7ee19..a5b75e4a 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderTests.cs @@ -186,5 +186,65 @@ namespace SharpCompress.Test.Tar } } } + + [Fact] + public void Tar_GZip_With_Symlink_Entries() + { + var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( + System.Runtime.InteropServices.OSPlatform.Windows); + using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz"))) + using (var reader = TarReader.Open(stream)) + { + List names = new List(); + while (reader.MoveToNextEntry()) + { + if (reader.Entry.IsDirectory) + { + continue; + } + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH, + new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true, + WriteSymbolicLink = (sourcePath, targetPath) => + { + if (!isWindows) + { + var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath); + if (System.IO.File.Exists(sourcePath)) + { + link.Delete(); // equivalent to ln -s -f + } + link.CreateSymbolicLinkTo(targetPath); + } + } + }); + if (!isWindows) + { + if (reader.Entry.LinkTarget != null) + { + var path = System.IO.Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key); + var link = new Mono.Unix.UnixSymbolicLinkInfo(path); + if (link.HasContents) + { + // need to convert the link to an absolute path for comparison + var target = reader.Entry.LinkTarget; + var realTarget = System.IO.Path.GetFullPath( + System.IO.Path.Combine($"{System.IO.Path.GetDirectoryName(path)}", + target) + ); + + Assert.Equal(realTarget, link.GetContents().ToString()); + } + else + { + Assert.True(false, "Symlink has no target"); + } + } + } + } + } + } } } diff --git a/tests/TestArchives/Archives/TarWithSymlink.tar.gz b/tests/TestArchives/Archives/TarWithSymlink.tar.gz new file mode 100644 index 00000000..4dce43df Binary files /dev/null and b/tests/TestArchives/Archives/TarWithSymlink.tar.gz differ