From 303b2ee437f022a5e020300378fc1350c14e5d90 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Wed, 29 Jul 2026 08:26:14 +0200 Subject: [PATCH 1/2] Add support for uncompressing `.tar.xz` archives # Conflicts: # src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs --- Directory.Packages.props | 3 +- .../CompressionExtensions.cs | 30 +++++++++++++++++++ .../Fallout.Utilities.IO.Compression.csproj | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f67c023b..ec07825a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,6 +26,7 @@ + @@ -90,4 +91,4 @@ - \ No newline at end of file + diff --git a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs index f6d8df56b..de163aaf6 100644 --- a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs +++ b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs @@ -8,6 +8,8 @@ using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; +using SharpCompress.Common; +using SharpCompress.Readers; using ZipFile = ICSharpCode.SharpZipLib.Zip.ZipFile; namespace Fallout.Common.IO; @@ -28,6 +30,10 @@ public static void CompressTo(this AbsolutePath directory, AbsolutePath archiveF { directory.TarBZip2To(archiveFile, filter); } + else if (archiveFile.HasExtension(".tar.xz", ".txz")) + { + Assert.Fail($"Compressing a .tar.xz archive currently not supported. Archive file: '{Path.GetFileName(archiveFile)}'"); + } else { Assert.Fail($"Unknown archive extension for archive '{Path.GetFileName(archiveFile)}'"); @@ -48,6 +54,10 @@ public static void UncompressTo(this AbsolutePath archiveFile, AbsolutePath dire { archiveFile.UnTarBZip2To(directory); } + else if (archiveFile.HasExtension(".tar.xz", ".txz")) + { + archiveFile.UnTarXzTo(directory); + } else { Assert.Fail($"Unknown archive extension for archive '{Path.GetFileName(archiveFile)}'"); @@ -149,6 +159,26 @@ public static void UnTarBZip2To(this AbsolutePath archiveFile, AbsolutePath dire UncompressTar(archiveFile, directory, x => new BZip2InputStream(x)); } + public static void UnTarXzTo(this AbsolutePath archive, AbsolutePath directory) + { + using Stream stream = File.OpenRead(archive); + using var reader = ReaderFactory.OpenReader(stream); + + while (reader.MoveToNextEntry()) + { + if (reader.Entry.IsDirectory) + { + continue; + } + + reader.WriteEntryToDirectory(directory, new ExtractionOptions + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + private static void CompressTar( AbsolutePath baseDirectory, AbsolutePath archiveFile, diff --git a/src/Fallout.Utilities.IO.Compression/Fallout.Utilities.IO.Compression.csproj b/src/Fallout.Utilities.IO.Compression/Fallout.Utilities.IO.Compression.csproj index 456306f23..7b6c8420a 100644 --- a/src/Fallout.Utilities.IO.Compression/Fallout.Utilities.IO.Compression.csproj +++ b/src/Fallout.Utilities.IO.Compression/Fallout.Utilities.IO.Compression.csproj @@ -10,6 +10,7 @@ + From 20307c3cec853e59f0c264f10befa68b56ace076 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Sat, 22 Aug 2026 18:29:10 +0200 Subject: [PATCH 2/2] Add XML summaries to methods --- .../CompressionExtensions.cs | 170 ++++++++++++++---- 1 file changed, 135 insertions(+), 35 deletions(-) diff --git a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs index de163aaf6..5b2705c25 100644 --- a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs +++ b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs @@ -16,6 +16,19 @@ namespace Fallout.Common.IO; public static class CompressionExtensions { + /// + /// Compresses into , choosing the compression format based on the + /// archive file's extension. + /// + /// The directory whose contents should be compressed. + /// + /// The archive file to create. Its extension determines the compression format (e.g. .zip, + /// .tar.gz, .tar.bz2). + /// + /// + /// An optional predicate used to filter which files are included in the archive. If null, all files are + /// included. + /// public static void CompressTo(this AbsolutePath directory, AbsolutePath archiveFile, Func filter = null) { if (archiveFile.HasExtension(".zip")) @@ -32,7 +45,8 @@ public static void CompressTo(this AbsolutePath directory, AbsolutePath archiveF } else if (archiveFile.HasExtension(".tar.xz", ".txz")) { - Assert.Fail($"Compressing a .tar.xz archive currently not supported. Archive file: '{Path.GetFileName(archiveFile)}'"); + Assert.Fail( + $"Compressing a .tar.xz archive currently not supported. Archive file: '{Path.GetFileName(archiveFile)}'"); } else { @@ -40,6 +54,15 @@ public static void CompressTo(this AbsolutePath directory, AbsolutePath archiveF } } + /// + /// Uncompresses into , choosing the decompression format based on + /// the archive file's extension. + /// + /// + /// The archive file to extract. Its extension determines the decompression format (e.g. .zip, + /// .tar.gz, .tar.bz2, .tar.xz). + /// + /// The directory into which the archive's contents are extracted. public static void UncompressTo(this AbsolutePath archiveFile, AbsolutePath directory) { if (archiveFile.HasExtension(".zip")) @@ -64,6 +87,17 @@ public static void UncompressTo(this AbsolutePath archiveFile, AbsolutePath dire } } + /// + /// Compresses into a ZIP archive at . + /// + /// The directory whose contents should be added to the ZIP archive. + /// The ZIP archive file to create. + /// + /// An optional predicate used to filter which files are included in the archive. If null, all files are + /// included. + /// + /// The compression level to use for the archive entries. + /// The used to open the archive file. public static void ZipTo( this AbsolutePath directory, AbsolutePath archiveFile, @@ -74,50 +108,71 @@ public static void ZipTo( archiveFile.Parent.CreateDirectory(); filter ??= _ => true; - var files = directory.GetFiles(depth: int.MaxValue).Where(filter).ToList(); + List files = directory.GetFiles(depth: int.MaxValue).Where(filter).ToList(); - using var fileStream = File.Open(archiveFile, fileMode, FileAccess.ReadWrite); - using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create); + using FileStream fileStream = File.Open(archiveFile, fileMode, FileAccess.ReadWrite); + using ZipArchive zipArchive = new(fileStream, ZipArchiveMode.Create); void AddFile(AbsolutePath file) { - var relativePath = directory.GetRelativePathTo(file); - var entryName = ZipEntry.CleanName(relativePath); + RelativePath relativePath = directory.GetRelativePathTo(file); + string entryName = ZipEntry.CleanName(relativePath); zipArchive.CreateEntryFromFile(file, entryName, compressionLevel); } files.ForEach(AddFile); } + /// + /// Extracts the contents of a ZIP archive at into . + /// Destination directory is created, and conflicting files are overwritten. + /// + /// The ZIP archive file to extract. + /// The directory into which the archive's contents are extracted. public static void UnZipTo(this AbsolutePath archiveFile, AbsolutePath directory) { - using var fileStream = File.OpenRead(archiveFile); - using var zipFile = new ZipFile(fileStream); + using FileStream fileStream = File.OpenRead(archiveFile); + using ZipFile zipFile = new(fileStream); - var entries = zipFile.Cast().Where(x => !x.IsDirectory); + IEnumerable entries = zipFile.Cast().Where(x => !x.IsDirectory); void HandleEntry(ZipEntry entry) { - var file = directory / entry.Name; + AbsolutePath file = directory / entry.Name; Directory.CreateDirectory(file.Parent.NotNull()); - using var entryStream = zipFile.GetInputStream(entry); - using var outputStream = File.Open(file, FileMode.Create); + using Stream entryStream = zipFile.GetInputStream(entry); + using FileStream outputStream = File.Open(file, FileMode.Create); entryStream.CopyTo(outputStream); } entries.ForEach(HandleEntry); } + /// + /// Compresses the given into a gzip-compressed tar archive at . + /// + /// The base directory used to compute the relative entry names of the archived files. + /// The tar.gz archive file to create. + /// The files to add to the archive. + /// The used to open the archive file. public static void TarGZipTo( this AbsolutePath baseDirectory, AbsolutePath archiveFile, IEnumerable files, - FileMode fileMode = FileMode.CreateNew) - { + FileMode fileMode = FileMode.CreateNew) => CompressTar(baseDirectory, archiveFile, files.ToList(), fileMode, x => new GZipOutputStream(x)); - } + /// + /// Compresses into a gzip-compressed tar archive at . + /// + /// The directory whose contents should be added to the archive. + /// The tar.gz archive file to create. + /// + /// An optional predicate used to filter which files are included in the archive. If null, all files are + /// included. + /// + /// The used to open the archive file. public static void TarGZipTo( this AbsolutePath directory, AbsolutePath archiveFile, @@ -125,19 +180,34 @@ public static void TarGZipTo( FileMode fileMode = FileMode.CreateNew) { filter ??= _ => true; - var files = directory.GetFiles(depth: int.MaxValue).Where(filter); + IEnumerable files = directory.GetFiles(depth: int.MaxValue).Where(filter); directory.TarGZipTo(archiveFile, files, fileMode); } + /// + /// Compresses the given into a bzip2-compressed tar archive at . + /// + /// The base directory used to compute the relative entry names of the archived files. + /// The tar.bz2 archive file to create. + /// The files to add to the archive. + /// The used to open the archive file. public static void TarBZip2To( this AbsolutePath directory, AbsolutePath archiveFile, IEnumerable files, - FileMode fileMode = FileMode.CreateNew) - { + FileMode fileMode = FileMode.CreateNew) => CompressTar(directory, archiveFile, files.ToList(), fileMode, x => new BZip2OutputStream(x)); - } + /// + /// Compresses into a bzip2-compressed tar archive at . + /// + /// The directory whose contents should be added to the archive. + /// The tar.bz2 archive file to create. + /// + /// An optional predicate used to filter which files are included in the archive. If null, all files are + /// included. + /// + /// The used to open the archive file. public static void TarBZip2To( this AbsolutePath directory, AbsolutePath archiveFile, @@ -145,24 +215,38 @@ public static void TarBZip2To( FileMode fileMode = FileMode.CreateNew) { filter ??= _ => true; - var files = directory.GetFiles(depth: int.MaxValue).Where(filter); + IEnumerable files = directory.GetFiles(depth: int.MaxValue).Where(filter); directory.TarBZip2To(archiveFile, files, fileMode); } - public static void UnTarGZipTo(this AbsolutePath archiveFile, AbsolutePath directory) - { + /// + /// Extracts the contents of a gzip-compressed tar archive at into . + /// Destination directory is created, and conflicting files are overwritten. + /// + /// The tar.gz archive file to extract. + /// The directory into which the archive's contents are extracted. + public static void UnTarGZipTo(this AbsolutePath archiveFile, AbsolutePath directory) => UncompressTar(archiveFile, directory, x => new GZipInputStream(x)); - } - public static void UnTarBZip2To(this AbsolutePath archiveFile, AbsolutePath directory) - { + /// + /// Extracts the contents of a bzip2-compressed tar archive at into . + /// Destination directory is created, and conflicting files are overwritten. + /// + /// The tar.bz2 archive file to extract. + /// The directory into which the archive's contents are extracted. + public static void UnTarBZip2To(this AbsolutePath archiveFile, AbsolutePath directory) => UncompressTar(archiveFile, directory, x => new BZip2InputStream(x)); - } + /// + /// Extracts the contents of an xz-compressed tar archive at into . + /// Destination directory is created, and conflicting files are skipped. + /// + /// The tar.xz archive file to extract. + /// The directory into which the archive's contents are extracted. public static void UnTarXzTo(this AbsolutePath archive, AbsolutePath directory) { using Stream stream = File.OpenRead(archive); - using var reader = ReaderFactory.OpenReader(stream); + using IReader reader = ReaderFactory.OpenReader(stream); while (reader.MoveToNextEntry()) { @@ -179,6 +263,15 @@ public static void UnTarXzTo(this AbsolutePath archive, AbsolutePath directory) } } + /// + /// Compresses the given into a tar archive at , wrapping the underlying + /// file stream with the compression stream produced by . + /// + /// The base directory used to compute the relative entry names of the archived files. + /// The archive file to create. + /// The files to add to the archive. + /// The used to open the archive file. + /// A factory that wraps the raw archive file stream with the desired compression stream. private static void CompressTar( AbsolutePath baseDirectory, AbsolutePath archiveFile, @@ -188,26 +281,33 @@ private static void CompressTar( { archiveFile.Parent.CreateDirectory(); - using var fileStream = File.Open(archiveFile, fileMode, FileAccess.ReadWrite); - using var outputStream = outputStreamFactory(fileStream); - using var tarArchive = TarArchive.CreateOutputTarArchive(outputStream); + using FileStream fileStream = File.Open(archiveFile, fileMode, FileAccess.ReadWrite); + using Stream outputStream = outputStreamFactory(fileStream); + using TarArchive tarArchive = TarArchive.CreateOutputTarArchive(outputStream); void AddFile(AbsolutePath file) { - var entry = TarEntry.CreateEntryFromFile(file); + TarEntry entry = TarEntry.CreateEntryFromFile(file); entry.Name = baseDirectory.GetUnixRelativePathTo(file); - tarArchive.WriteEntry(entry, recurse: false); + tarArchive.WriteEntry(entry, false); } files.ForEach(AddFile); } + /// + /// Extracts the contents of a tar archive at into , wrapping the + /// underlying file stream with the decompression stream produced by . + /// + /// The archive file to extract. + /// The directory into which the archive's contents are extracted. + /// A factory that wraps the raw archive file stream with the desired decompression stream. private static void UncompressTar(AbsolutePath archiveFile, AbsolutePath directory, Func inputStreamFactory) { - using var fileStream = File.OpenRead(archiveFile); - using var inputStream = inputStreamFactory(fileStream); - using var tarArchive = TarArchive.CreateInputTarArchive(inputStream, nameEncoding: null); + using FileStream fileStream = File.OpenRead(archiveFile); + using Stream inputStream = inputStreamFactory(fileStream); + using TarArchive tarArchive = TarArchive.CreateInputTarArchive(inputStream, null); directory.CreateDirectory();