From 2976eb038f73c46fe78da36f167c583db1df93ad 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 --- Directory.Packages.props | 3 ++- .../CompressionExtensions.cs | 26 +++++++++++++++++++ .../Fallout.Utilities.IO.Compression.csproj | 1 + 3 files changed, 29 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 85c69b524..c3a67005d 100644 --- a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs +++ b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs @@ -7,6 +7,8 @@ using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; +using SharpCompress.Common; +using SharpCompress.Readers; using Fallout.Common.Utilities.Collections; namespace Fallout.Common.IO; @@ -21,6 +23,8 @@ public static void CompressTo(this AbsolutePath directory, AbsolutePath archiveF directory.TarGZipTo(archiveFile, filter); else if (archiveFile.HasExtension(".tar.bz2", ".tbz2", ".tbz")) 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)}'"); } @@ -33,6 +37,8 @@ public static void UncompressTo(this AbsolutePath archiveFile, AbsolutePath dire archiveFile.UnTarGZipTo(directory); else if (archiveFile.HasExtension(".tar.bz2", ".tbz2", ".tbz")) archiveFile.UnTarBZip2To(directory); + else if (archiveFile.HasExtension(".tar.xz", ".txz")) + archiveFile.UnTarXzTo(directory); else Assert.Fail($"Unknown archive extension for archive '{Path.GetFileName(archiveFile)}'"); } @@ -132,6 +138,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 73acf7b15ff9b893e3fd03a98b61237c7ab2e0e6 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 | 193 ++++++++++++++---- 1 file changed, 157 insertions(+), 36 deletions(-) diff --git a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs index c3a67005d..5b2705c25 100644 --- a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs +++ b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs @@ -3,46 +3,101 @@ using System.IO; using System.IO.Compression; using System.Linq; +using Fallout.Common.Utilities.Collections; using ICSharpCode.SharpZipLib.BZip2; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; using SharpCompress.Common; using SharpCompress.Readers; -using Fallout.Common.Utilities.Collections; +using ZipFile = ICSharpCode.SharpZipLib.Zip.ZipFile; 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")) + { directory.ZipTo(archiveFile, filter); + } else if (archiveFile.HasExtension(".tar.gz", ".tgz")) + { directory.TarGZipTo(archiveFile, filter); + } else if (archiveFile.HasExtension(".tar.bz2", ".tbz2", ".tbz")) + { 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)}'"); + { + 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)}'"); + } } + /// + /// 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")) + { archiveFile.UnZipTo(directory); + } else if (archiveFile.HasExtension(".tar.gz", ".tgz")) + { archiveFile.UnTarGZipTo(directory); + } else if (archiveFile.HasExtension(".tar.bz2", ".tbz2", ".tbz")) + { archiveFile.UnTarBZip2To(directory); + } else if (archiveFile.HasExtension(".tar.xz", ".txz")) + { archiveFile.UnTarXzTo(directory); + } else + { Assert.Fail($"Unknown archive extension for archive '{Path.GetFileName(archiveFile)}'"); + } } + /// + /// 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, @@ -53,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 ICSharpCode.SharpZipLib.Zip.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, @@ -104,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, @@ -124,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()) { @@ -158,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, @@ -167,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();