From 608c43d1c44e819c6e500864e2483ecb0f3f630c Mon Sep 17 00:00:00 2001 From: Olof Lagerkvist Date: Sun, 8 Sep 2024 17:15:30 +0200 Subject: [PATCH] Add support other compression methods for squashfs (#16) * Update .gitignore with standard VS/Resharper and various stuffs * Add PolySharp to support init properties * Add support for other compression methods to squashfs * Fix options to only emit them if necessary. Optimize MemoryStream usage for compressed streams. * Add support other compression methods for squashfs (#14) * Update .gitignore with standard VS/Resharper and various stuffs * Add PolySharp to support init properties * Add support for other compression methods to squashfs * Fix options to only emit them if necessary. Optimize MemoryStream usage for compressed streams. * Fixed/reverted some changes * Corrected mistake in target framework versions --------- Co-authored-by: Alexandre Mutel --- .gitignore | 368 ++++++++++++++++++ DiscUtils.sln.DotSettings | 5 +- Library/Directory.Build.props | 9 + Library/DiscUtils.SquashFs/BuilderContext.cs | 5 + .../DiscUtils.SquashFs/CompressionOptions.cs | 326 ++++++++++++++++ Library/DiscUtils.SquashFs/FragmentWriter.cs | 2 +- Library/DiscUtils.SquashFs/IdTableWriter.cs | 2 +- .../DiscUtils.SquashFs/MemoryStreamHelper.cs | 44 +++ Library/DiscUtils.SquashFs/Metablock.cs | 6 + Library/DiscUtils.SquashFs/MetablockWriter.cs | 17 +- .../SquashFileSystemBuilder.cs | 56 ++- .../SquashFileSystemBuilderOptions.cs | 78 ++++ .../SquashFileSystemCompressionKind.cs | 62 +++ .../SquashFileSystemReader.cs | 9 + .../SquashFileSystemReaderOptions.cs | 43 ++ .../StreamCompressorDelegate.cs | 30 ++ Library/DiscUtils.SquashFs/SuperBlock.cs | 17 +- .../VfsSquashFileSystemReader.cs | 38 +- Tests/LibraryTests/LibraryTests.csproj | 56 +-- .../SquashFs/SquashFileSystemBuilderTest.cs | 128 ++++++ 20 files changed, 1232 insertions(+), 69 deletions(-) create mode 100644 Library/DiscUtils.SquashFs/CompressionOptions.cs create mode 100644 Library/DiscUtils.SquashFs/MemoryStreamHelper.cs create mode 100644 Library/DiscUtils.SquashFs/SquashFileSystemBuilderOptions.cs create mode 100644 Library/DiscUtils.SquashFs/SquashFileSystemCompressionKind.cs create mode 100644 Library/DiscUtils.SquashFs/SquashFileSystemReaderOptions.cs create mode 100644 Library/DiscUtils.SquashFs/StreamCompressorDelegate.cs diff --git a/.gitignore b/.gitignore index 119f5ba6b..92698fc06 100644 --- a/.gitignore +++ b/.gitignore @@ -4,16 +4,384 @@ Build/ Debug/ Release/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser *.suo *.user *.lock.json LaunchSettings.json +*.userosscache +*.sln.docstates + +# Rider +.idea/ + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ +# Visual Studio 2015/2017 cache/options directory .vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ # Tests Tests/LibraryTests/TestResults/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage.*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages *.nupkg +# NuGet Symbol Packages *.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* *~ /testenvironments.json +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Rust +/lib/blake3_dotnet/target +Cargo.lock + +# Tmp folders +tmp/ +[Tt]emp/ + +# Remove artifacts produced by dotnet-releaser +artifacts-dotnet-releaser/ + +# Verify +*.received.* \ No newline at end of file diff --git a/DiscUtils.sln.DotSettings b/DiscUtils.sln.DotSettings index 27416e2dc..d0ab9c465 100644 --- a/DiscUtils.sln.DotSettings +++ b/DiscUtils.sln.DotSettings @@ -214,6 +214,8 @@ UseExplicitType <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> True True True @@ -221,4 +223,5 @@ True True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/Library/Directory.Build.props b/Library/Directory.Build.props index 7d234b614..1c7dfde9a 100644 --- a/Library/Directory.Build.props +++ b/Library/Directory.Build.props @@ -28,4 +28,13 @@ false false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Library/DiscUtils.SquashFs/BuilderContext.cs b/Library/DiscUtils.SquashFs/BuilderContext.cs index 7f0e829ea..e17d5602f 100644 --- a/Library/DiscUtils.SquashFs/BuilderContext.cs +++ b/Library/DiscUtils.SquashFs/BuilderContext.cs @@ -20,6 +20,7 @@ // DEALINGS IN THE SOFTWARE. // +using System; using System.IO; namespace DiscUtils.SquashFs; @@ -42,4 +43,8 @@ internal sealed class BuilderContext public WriteDataBlock WriteDataBlock { get; set; } public WriteFragment WriteFragment { get; set; } + + public StreamCompressorDelegate Compressor { get; set; } + + public MemoryStream SharedMemoryStream { get; set; } } \ No newline at end of file diff --git a/Library/DiscUtils.SquashFs/CompressionOptions.cs b/Library/DiscUtils.SquashFs/CompressionOptions.cs new file mode 100644 index 000000000..fd6c6f061 --- /dev/null +++ b/Library/DiscUtils.SquashFs/CompressionOptions.cs @@ -0,0 +1,326 @@ +// +// Copyright (c) 2024, Olof Lagerkvist and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.IO; +using DiscUtils.Streams; +using DiscUtils.Streams.Compatibility; + +namespace DiscUtils.SquashFs; + +/// +/// Base class for compression options. +/// +public abstract class CompressionOptions : IByteArraySerializable +{ + internal CompressionOptions(SquashFileSystemCompressionKind kind) + { + Kind = kind; + } + + /// + /// The kind of compression. + /// + public SquashFileSystemCompressionKind Kind { get; } + + + /// + public virtual int Size => 0; + + /// + public virtual int ReadFrom(ReadOnlySpan buffer) => Size; + + /// + public virtual void WriteTo(Span buffer) + { + } + + internal static int ConfigureCompressionAndGetTotalSize(SuperBlock block, CompressionOptions options) + { + if (block.Compression == SquashFileSystemCompressionKind.Lz4) + { + // LZ4 always has options + block.Flags |= SuperBlock.SuperBlockFlags.CompressorOptionsPresent; + return 8 + sizeof(ushort); // Account for the preamble ushort size + } + + if (block.Compression == SquashFileSystemCompressionKind.Xz) + { + if (options != null) + { + block.Flags |= SuperBlock.SuperBlockFlags.CompressorOptionsPresent; + return options.Size + sizeof(ushort); // Account for the preamble ushort size + } + } + + return 0; + } + + internal static CompressionOptions ReadFrom(Stream stream, SuperBlock block) + { + var hasOptions = (block.Flags & SuperBlock.SuperBlockFlags.CompressorOptionsPresent) != 0; + + switch (block.Compression) + { + case SquashFileSystemCompressionKind.Lz4: + { + // LZ4 always has options + Lz4CompressionOptions opts = new(); + var size = ReadCompressionSize(stream); + if (size < opts.Size) + { + throw new InvalidDataException($"Invalid LZ4 options size {size}. Expecting at least {opts.Size}"); + } + + opts.ReadFrom(stream, size); + if (opts.Version != Lz4CompressionFormatVersion.Legacy) + { + throw new NotSupportedException($"Unsupported LZ4 version {(uint)opts.Version}"); + } + + return new Lz4CompressionOptions(); + } + + case SquashFileSystemCompressionKind.Xz: + { + XzCompressionOptions opts = new(Math.Max(Metablock.SQUASHFS_METADATA_SIZE, (int)block.BlockSize)); + if (hasOptions) + { + var size = ReadCompressionSize(stream); + if (size < opts.Size) + { + throw new InvalidDataException($"Invalid Xz options size {size}. Expecting at least {opts.Size}"); + } + + opts.ReadFrom(stream, size); + } + return opts; + } + + case SquashFileSystemCompressionKind.ZLib: + { + ThrowIfHasOptions(); + return new ZLibCompressionOptions(); + } + case SquashFileSystemCompressionKind.Lzo: + { + ThrowIfHasOptions(); + return new LzoCompressionOptions(); + } + case SquashFileSystemCompressionKind.Lzma: + { + ThrowIfHasOptions(); + return new LzmaCompressionOptions(); + } + case SquashFileSystemCompressionKind.ZStd: + { + ThrowIfHasOptions(); + return new ZStdCompressionOptions(); + } + default: + { + throw new NotSupportedException($"Unsupported compression mode {(uint)block.Compression}"); + } + } + + void ThrowIfHasOptions() + { + if (hasOptions) + { + throw new NotSupportedException($"Unsupported compression options for {block.Compression}"); + } + } + } + + internal static void WriteTo(Stream stream, SuperBlock block, CompressionOptions options) + { + if (block.Compression == SquashFileSystemCompressionKind.Lz4) + { + // LZ4 always has options + options ??= new Lz4CompressionOptions(); + Span data = stackalloc byte[options.Size]; + WriteCompressionSize(stream, (ushort)options.Size); + options.WriteTo(data); + stream.Write(data); + } + else if (block.Compression == SquashFileSystemCompressionKind.Xz) + { + if (options != null) + { + Span data = stackalloc byte[options.Size]; + WriteCompressionSize(stream, (ushort)options.Size); + options.WriteTo(data); + stream.Write(data); + } + } + } + + internal static ushort ReadCompressionSize(Stream stream) + { + Span buffer = stackalloc byte[2]; + stream.ReadExactly(buffer); + var size = EndianUtilities.ToUInt16LittleEndian(buffer); + if ((size & Metablock.SQUASHFS_COMPRESSED_BIT) == 0) // Flag to indicate uncompressed buffer + { + throw new InvalidDataException("Invalid compressed size."); + } + return (ushort)(size & Metablock.SQUASHFS_COMPRESSED_BIT_SIZE_MASK); + } + + internal static void WriteCompressionSize(Stream stream, ushort size) + { + size |= Metablock.SQUASHFS_COMPRESSED_BIT; // Flag to indicate uncompressed buffer + Span buffer = stackalloc byte[2]; + EndianUtilities.WriteBytesLittleEndian(size, buffer); + stream.Write(buffer); + } +} + +/// +/// Compression options for LZ4. +/// +public class Lz4CompressionOptions : CompressionOptions +{ + private Lz4CompressionFormatVersion _version; + private bool _highCompression; + private const uint LZ4_HC = 1; + + /// + /// Creates a new instance of the Lz4 compression options. + /// + public Lz4CompressionOptions() : base(SquashFileSystemCompressionKind.Lz4) + { + Version = Lz4CompressionFormatVersion.Legacy; + } + + /// + /// Gets or sets the version of the LZ4 compression format. Only Legacy is supported + /// + public Lz4CompressionFormatVersion Version + { + get => _version; + init => _version = value; + } + + /// + /// Gets or sets a value indicating whether high compression is enabled. + /// + public bool HighCompression + { + get => _highCompression; + init => _highCompression = value; + } + + /// + public override int Size => 8; + + /// + public override int ReadFrom(ReadOnlySpan buffer) + { + _version = (Lz4CompressionFormatVersion)EndianUtilities.ToInt32LittleEndian(buffer); + // Read flags but ignore them + _highCompression = EndianUtilities.ToInt32LittleEndian(buffer.Slice(4)) == LZ4_HC; + return Size; + } + + /// + public override void WriteTo(Span buffer) + { + EndianUtilities.WriteBytesLittleEndian((uint)_version, buffer); + EndianUtilities.WriteBytesLittleEndian(_highCompression ? LZ4_HC : 0, buffer.Slice(4)); + } +} + +/// +/// Defines the version of the LZ4 compression format. +/// +public enum Lz4CompressionFormatVersion +{ + /// + /// Undefined. + /// + Undefined = 0, + + /// + /// The legacy version of LZ4. + /// + Legacy = 1, +} + +/// +/// Compression options for XZ. +/// +public class XzCompressionOptions : CompressionOptions +{ + private int _dictionarySize; + + /// + /// Creates a new instance of the XZ compression options. + /// + /// The size of the dictionary. + public XzCompressionOptions(int dictionarySize) : base(SquashFileSystemCompressionKind.Xz) + { + _dictionarySize = dictionarySize; + } + + /// + /// Gets the size of the dictionary. + /// + public int DictionarySize => _dictionarySize; + + /// + public override int Size => 8; + + /// + public override int ReadFrom(ReadOnlySpan buffer) + { + _dictionarySize = EndianUtilities.ToInt32LittleEndian(buffer); + // Read flags but ignore them + _ = EndianUtilities.ToInt32LittleEndian(buffer.Slice(4)); + return Size; + } + + /// + public override void WriteTo(Span buffer) + { + EndianUtilities.WriteBytesLittleEndian(_dictionarySize, buffer); + EndianUtilities.WriteBytesLittleEndian((uint)0, buffer.Slice(4)); + } +} + +/// +/// Compression options for ZLib. (No options are supported). +/// +public class ZLibCompressionOptions() : CompressionOptions(SquashFileSystemCompressionKind.ZLib); + +/// +/// Compression options for LZO. (No options are supported). +/// +public class LzoCompressionOptions() : CompressionOptions(SquashFileSystemCompressionKind.Lzo); + +/// +/// Compression options for LZMA. (No options are supported). +/// +public class LzmaCompressionOptions() : CompressionOptions(SquashFileSystemCompressionKind.Lzma); + +/// +/// ZStandard compression options. (No options are supported). +/// +public class ZStdCompressionOptions() : CompressionOptions(SquashFileSystemCompressionKind.ZStd); \ No newline at end of file diff --git a/Library/DiscUtils.SquashFs/FragmentWriter.cs b/Library/DiscUtils.SquashFs/FragmentWriter.cs index 7459d0137..6a286e824 100644 --- a/Library/DiscUtils.SquashFs/FragmentWriter.cs +++ b/Library/DiscUtils.SquashFs/FragmentWriter.cs @@ -98,7 +98,7 @@ internal long Persist() _fragmentBlocks[i].WriteTo(buffer, i * recordSize); } - var writer = new MetablockWriter(); + var writer = new MetablockWriter(_context); writer.Write(buffer, 0, _fragmentBlocks.Count * recordSize); writer.Persist(_context.RawStream); } diff --git a/Library/DiscUtils.SquashFs/IdTableWriter.cs b/Library/DiscUtils.SquashFs/IdTableWriter.cs index 8e5a8148e..860d958a3 100644 --- a/Library/DiscUtils.SquashFs/IdTableWriter.cs +++ b/Library/DiscUtils.SquashFs/IdTableWriter.cs @@ -79,7 +79,7 @@ internal long Persist() // Persist the actual Id's var blockPos = _context.RawStream.Position; - var writer = new MetablockWriter(); + var writer = new MetablockWriter(_context); writer.Write(_context.IoBuffer, 0, _ids.Count * 4); writer.Persist(_context.RawStream); diff --git a/Library/DiscUtils.SquashFs/MemoryStreamHelper.cs b/Library/DiscUtils.SquashFs/MemoryStreamHelper.cs new file mode 100644 index 000000000..05b36768e --- /dev/null +++ b/Library/DiscUtils.SquashFs/MemoryStreamHelper.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) 2024, Olof Lagerkvist and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.IO; + +namespace DiscUtils.SquashFs; + +internal static class MemoryStreamHelper +{ + public static MemoryStream CreateWithFixedCapacity(int capacity) + { +#if NET6_0_OR_GREATER + var array = GC.AllocateUninitializedArray(capacity); +#else + + var array = new byte[capacity]; +#endif + return new MemoryStream(array, 0, capacity, true, true); + } + + public static MemoryStream Initialize(MemoryStream stream) + { + stream.SetLength(0); + return stream; + } +} \ No newline at end of file diff --git a/Library/DiscUtils.SquashFs/Metablock.cs b/Library/DiscUtils.SquashFs/Metablock.cs index 71047b135..42dcd3298 100644 --- a/Library/DiscUtils.SquashFs/Metablock.cs +++ b/Library/DiscUtils.SquashFs/Metablock.cs @@ -26,5 +26,11 @@ namespace DiscUtils.SquashFs; internal sealed class Metablock : Block { + public const int SQUASHFS_COMPRESSED_BIT = 1 << 15; + + public const int SQUASHFS_COMPRESSED_BIT_SIZE_MASK = ~SQUASHFS_COMPRESSED_BIT; + + public const int SQUASHFS_METADATA_SIZE = 8192; + public long NextBlockStart { get; set; } } \ No newline at end of file diff --git a/Library/DiscUtils.SquashFs/MetablockWriter.cs b/Library/DiscUtils.SquashFs/MetablockWriter.cs index e855deb4e..a1ebd0ae1 100644 --- a/Library/DiscUtils.SquashFs/MetablockWriter.cs +++ b/Library/DiscUtils.SquashFs/MetablockWriter.cs @@ -33,13 +33,18 @@ namespace DiscUtils.SquashFs; internal sealed class MetablockWriter : IDisposable { private MemoryStream _buffer; + private readonly StreamCompressorDelegate _compressor; + private readonly MemoryStream _sharedMemoryStream; private readonly byte[] _currentBlock; private int _currentBlockNum; private int _currentOffset; - public MetablockWriter() + public MetablockWriter(BuilderContext context) { + _compressor = context.Compressor; + _sharedMemoryStream = context.SharedMemoryStream; + _currentBlock = new byte[8 * 1024]; _buffer = new MemoryStream(); } @@ -98,10 +103,8 @@ internal long DistanceFrom(MetadataRef startPos) private void NextBlock() { - const int SQUASHFS_COMPRESSED_BIT = 1 << 15; - - var compressed = new MemoryStream(); - using (var compStream = new ZlibStream(compressed, CompressionMode.Compress, true)) + var compressed = MemoryStreamHelper.Initialize(_sharedMemoryStream); + using (var compStream = _compressor(compressed)) { compStream.Write(_currentBlock, 0, _currentOffset); } @@ -118,13 +121,13 @@ private void NextBlock() else { writeData = _currentBlock; - writeLen = (ushort)(_currentOffset | SQUASHFS_COMPRESSED_BIT); + writeLen = (ushort)(_currentOffset | Metablock.SQUASHFS_COMPRESSED_BIT); } Span header = stackalloc byte[2]; EndianUtilities.WriteBytesLittleEndian(writeLen, header); _buffer.Write(header); - _buffer.Write(writeData.Slice(0, writeLen & 0x7FFF)); + _buffer.Write(writeData.Slice(0, writeLen & Metablock.SQUASHFS_COMPRESSED_BIT_SIZE_MASK)); ++_currentBlockNum; } diff --git a/Library/DiscUtils.SquashFs/SquashFileSystemBuilder.cs b/Library/DiscUtils.SquashFs/SquashFileSystemBuilder.cs index 87c4228a6..4f855b869 100644 --- a/Library/DiscUtils.SquashFs/SquashFileSystemBuilder.cs +++ b/Library/DiscUtils.SquashFs/SquashFileSystemBuilder.cs @@ -39,11 +39,13 @@ namespace DiscUtils.SquashFs; /// public sealed class SquashFileSystemBuilder : StreamBuilder, IFileSystemBuilder { - private const int DefaultBlockSize = 131072; + internal const int DefaultBlockSize = 131072; + private BuilderContext _context; private uint _nextInode; private BuilderDirectory _rootDir; + private readonly StreamCompressorDelegate _compressor; // Progress reporting event public event EventHandler ProgressChanged; @@ -65,7 +67,15 @@ private void AddProgress(int newFiles, int newItems) /// /// Initializes a new instance of the SquashFileSystemBuilder class. /// - public SquashFileSystemBuilder() + public SquashFileSystemBuilder() : this(null) + { + } + + /// + /// Initializes a new instance of the SquashFileSystemBuilder class. + /// + /// The options for this builder. + public SquashFileSystemBuilder(SquashFileSystemBuilderOptions options) { DefaultFilePermissions = UnixFilePermissions.OwnerRead | UnixFilePermissions.OwnerWrite | UnixFilePermissions.GroupRead | UnixFilePermissions.GroupWrite; @@ -74,8 +84,16 @@ public SquashFileSystemBuilder() UnixFilePermissions.OthersExecute; DefaultUser = 0; DefaultGroup = 0; + + Options = options ?? SquashFileSystemBuilderOptions.Default; + _compressor = Options.ResolveCompressor(); } + /// + /// Gets the options for this builder. + /// + public SquashFileSystemBuilderOptions Options { get; } + /// /// Gets or sets the default permissions used for new directories. /// @@ -399,11 +417,13 @@ public override void Build(Stream output) { RawStream = output, DataBlockSize = DefaultBlockSize, - IoBuffer = new byte[DefaultBlockSize] + IoBuffer = new byte[DefaultBlockSize], + SharedMemoryStream = MemoryStreamHelper.CreateWithFixedCapacity(DefaultBlockSize), + Compressor = _compressor }; - var inodeWriter = new MetablockWriter(); - var dirWriter = new MetablockWriter(); + var inodeWriter = new MetablockWriter(_context); + var dirWriter = new MetablockWriter(_context); var fragWriter = new FragmentWriter(_context); var idWriter = new IdTableWriter(_context); @@ -421,13 +441,16 @@ public override void Build(Stream output) Magic = SuperBlock.SquashFsMagic, CreationTime = DateTime.Now, BlockSize = (uint)_context.DataBlockSize, - Compression = SuperBlock.CompressionType.ZLib + Compression = Options.CompressionKind }; superBlock.BlockSizeLog2 = (ushort)MathUtilities.Log2(superBlock.BlockSize); superBlock.MajorVersion = 4; superBlock.MinorVersion = 0; - output.Position = superBlock.Size; + // Set flags before has it might be changed by CompressionOptions + superBlock.Flags = SuperBlock.SuperBlockFlags.NoXAttrs | SuperBlock.SuperBlockFlags.FragmentsAlwaysGenerated; + var compressionOptionsSize = CompressionOptions.ConfigureCompressionAndGetTotalSize(superBlock, Options.CompressionOptions); + output.Position = superBlock.Size + compressionOptionsSize; GetRoot().Reset(); GetRoot().Write(_context); @@ -436,7 +459,6 @@ public override void Build(Stream output) superBlock.InodesCount = _nextInode - 1; superBlock.FragmentsCount = (uint)fragWriter.FragmentBlocksCount; superBlock.UidGidCount = (ushort)idWriter.IdCount; - superBlock.Flags = SuperBlock.SuperBlockFlags.NoXAttrs | SuperBlock.SuperBlockFlags.FragmentsAlwaysGenerated; superBlock.InodeTableStart = output.Position; inodeWriter.Persist(output); @@ -465,6 +487,10 @@ public override void Build(Stream output) Span buffer = stackalloc byte[superBlock.Size]; superBlock.WriteTo(buffer); output.Write(buffer); + + // Add optional compression options + CompressionOptions.WriteTo(output, superBlock, Options.CompressionOptions); + output.Position = end; } @@ -501,10 +527,10 @@ private uint WriteDataBlock(byte[] buffer, int offset, int count) private uint WriteDataBlock(ReadOnlySpan buffer) { - const int SQUASHFS_COMPRESSED_BIT = 1 << 24; + const int SQUASHFS_COMPRESSED_BIT_BLOCK = 1 << 24; - var compressed = new MemoryStream(); - using (var compStream = new ZlibStream(compressed, CompressionMode.Compress, true)) + var compressed = MemoryStreamHelper.Initialize(_context.SharedMemoryStream); + using (var compStream = _compressor(compressed)) { compStream.Write(buffer); } @@ -515,14 +541,18 @@ private uint WriteDataBlock(ReadOnlySpan buffer) if (compressed.Length < buffer.Length) { var compressedData = compressed.AsSpan(); - compressedData[1] = 0xda; + + if (Options.CompressionKind == SquashFileSystemCompressionKind.ZLib) + { + compressedData[1] = 0xda; // Set Best compression level for zlib header + } writeData = compressedData; returnValue = writeData.Length; } else { writeData = buffer; - returnValue = writeData.Length | SQUASHFS_COMPRESSED_BIT; // Flag to indicate uncompressed buffer + returnValue = writeData.Length | SQUASHFS_COMPRESSED_BIT_BLOCK; // Flag to indicate uncompressed buffer } _context.RawStream.Write(writeData); diff --git a/Library/DiscUtils.SquashFs/SquashFileSystemBuilderOptions.cs b/Library/DiscUtils.SquashFs/SquashFileSystemBuilderOptions.cs new file mode 100644 index 000000000..50b1924a3 --- /dev/null +++ b/Library/DiscUtils.SquashFs/SquashFileSystemBuilderOptions.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) 2024, Olof Lagerkvist and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.IO.Compression; +using DiscUtils.Compression; + +namespace DiscUtils.SquashFs; + +/// +/// Options for building a SquashFs file system. +/// +public sealed class SquashFileSystemBuilderOptions +{ + /// + /// Default options for building a SquashFs file system. + /// + public static SquashFileSystemBuilderOptions Default = new(); + + + /// + /// Gets or sets the compression kind to use for the file system. + /// + public SquashFileSystemCompressionKind CompressionKind { get; init; } = SquashFileSystemCompressionKind.ZLib; + + /// + /// Gets or sets the compression options to use for the file system. The options must be compatible with the . Default is null. + /// + public CompressionOptions CompressionOptions { get; init; } + + /// + /// Gets or sets the compressor resolver for the file system. + /// + public Func GetCompressor { get; init; } + + /// + /// Resolves the compressor for the specified compression. + /// + /// The compressor. + /// If no compressor was found for the specified compression. + internal StreamCompressorDelegate ResolveCompressor() + { + StreamCompressorDelegate compressor = null; + if (CompressionKind == SquashFileSystemCompressionKind.ZLib) + { + compressor = static stream => new ZlibStream(stream, CompressionMode.Compress, true); + } + + if (GetCompressor != null) + { + compressor = GetCompressor(CompressionKind , CompressionOptions); + } + + if (compressor == null) + { + throw new InvalidOperationException($"No compressor found for the specified compression {CompressionOptions}"); + } + + return compressor; + } +} \ No newline at end of file diff --git a/Library/DiscUtils.SquashFs/SquashFileSystemCompressionKind.cs b/Library/DiscUtils.SquashFs/SquashFileSystemCompressionKind.cs new file mode 100644 index 000000000..6f5da19ed --- /dev/null +++ b/Library/DiscUtils.SquashFs/SquashFileSystemCompressionKind.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) 2024, Olof Lagerkvist and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +namespace DiscUtils.SquashFs; + +/// +/// The compression algorithm used in the SquashFs file system. +/// +public enum SquashFileSystemCompressionKind : ushort +{ + /// + /// The compression algorithm is unknown. + /// + Unknown, + + /// + /// The compression algorithm is ZLib. + /// + ZLib, + + /// + /// The compression algorithm is Lzma. + /// + Lzma, + + /// + /// The compression algorithm is Lzo. + /// + Lzo, + + /// + /// The compression algorithm is Xz. + /// + Xz, + + /// + /// The compression algorithm is Lz4. + /// + Lz4, + + /// + /// The compression algorithm is ZStd. + /// + ZStd +} diff --git a/Library/DiscUtils.SquashFs/SquashFileSystemReader.cs b/Library/DiscUtils.SquashFs/SquashFileSystemReader.cs index 85fbee2f9..1f2f045a6 100644 --- a/Library/DiscUtils.SquashFs/SquashFileSystemReader.cs +++ b/Library/DiscUtils.SquashFs/SquashFileSystemReader.cs @@ -20,6 +20,7 @@ // DEALINGS IN THE SOFTWARE. // +using System; using System.IO; using DiscUtils.Streams; using DiscUtils.Vfs; @@ -42,6 +43,14 @@ public class SquashFileSystemReader : VfsFileSystemFacade, IUnixFileSystem public SquashFileSystemReader(Stream data) : base(new VfsSquashFileSystemReader(data)) {} + /// + /// Initializes a new instance of the SquashFileSystemReader class. + /// + /// The stream to read the file system image from. + /// The options for this reader. + public SquashFileSystemReader(Stream data, SquashFileSystemReaderOptions options) + : base(new VfsSquashFileSystemReader(data, options)) { } + /// /// Gets Unix file information about a file or directory. /// diff --git a/Library/DiscUtils.SquashFs/SquashFileSystemReaderOptions.cs b/Library/DiscUtils.SquashFs/SquashFileSystemReaderOptions.cs new file mode 100644 index 000000000..708383eb5 --- /dev/null +++ b/Library/DiscUtils.SquashFs/SquashFileSystemReaderOptions.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2024, Olof Lagerkvist and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.IO; + +namespace DiscUtils.SquashFs; + +/// +/// Options for the SquashFs file system reader. +/// +public sealed class SquashFileSystemReaderOptions +{ + /// + /// Gets or sets the decompressor resolver for the file system. + /// + public GetDecompressorDelegate GetDecompressor { get; init; } +} + +/// +/// Delegate to get a decompressor for a specific compression kind. +/// +/// The kind of compression +/// Optional option. Can be null. +/// A function to decode a stream of the specified compression. Returns null if not supported. +public delegate StreamCompressorDelegate GetDecompressorDelegate(SquashFileSystemCompressionKind compressionKind, CompressionOptions compressionOptions); \ No newline at end of file diff --git a/Library/DiscUtils.SquashFs/StreamCompressorDelegate.cs b/Library/DiscUtils.SquashFs/StreamCompressorDelegate.cs new file mode 100644 index 000000000..b2e7d4563 --- /dev/null +++ b/Library/DiscUtils.SquashFs/StreamCompressorDelegate.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) 2024, Olof Lagerkvist and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System.IO; + +namespace DiscUtils.SquashFs; + +/// +/// Delegate for decompressing/compressing a stream. +/// +/// The stream to compress or decompress +/// The stream wrapped +public delegate Stream StreamCompressorDelegate(MemoryStream stream); \ No newline at end of file diff --git a/Library/DiscUtils.SquashFs/SuperBlock.cs b/Library/DiscUtils.SquashFs/SuperBlock.cs index 5bc80319c..232981910 100644 --- a/Library/DiscUtils.SquashFs/SuperBlock.cs +++ b/Library/DiscUtils.SquashFs/SuperBlock.cs @@ -27,16 +27,7 @@ namespace DiscUtils.SquashFs; internal class SuperBlock : IByteArraySerializable { - public enum CompressionType : ushort - { - Unknown, - ZLib, - LZMA, - Lzo, - Xz, - Lz4, - ZStd - } + [Flags] public enum SuperBlockFlags : ushort @@ -58,7 +49,7 @@ public enum SuperBlockFlags : ushort public uint BlockSize; public ushort BlockSizeLog2; public long BytesUsed; - public CompressionType Compression; + public SquashFileSystemCompressionKind Compression; public DateTime CreationTime; public long DirectoryTableStart; public long ExtendedAttrsTableStart; @@ -90,7 +81,7 @@ public int ReadFrom(ReadOnlySpan buffer) CreationTime = DateTimeOffset.FromUnixTimeSeconds(EndianUtilities.ToUInt32LittleEndian(buffer.Slice(8))).DateTime; BlockSize = EndianUtilities.ToUInt32LittleEndian(buffer.Slice(12)); FragmentsCount = EndianUtilities.ToUInt32LittleEndian(buffer.Slice(16)); - Compression = (CompressionType)EndianUtilities.ToUInt16LittleEndian(buffer.Slice(20)); + Compression = (SquashFileSystemCompressionKind)EndianUtilities.ToUInt16LittleEndian(buffer.Slice(20)); BlockSizeLog2 = EndianUtilities.ToUInt16LittleEndian(buffer.Slice(22)); Flags = (SuperBlockFlags)EndianUtilities.ToUInt16LittleEndian(buffer.Slice(24)); UidGidCount = EndianUtilities.ToUInt16LittleEndian(buffer.Slice(26)); @@ -130,4 +121,4 @@ public void WriteTo(Span buffer) EndianUtilities.WriteBytesLittleEndian(FragmentTableStart, buffer.Slice(80)); EndianUtilities.WriteBytesLittleEndian(LookupTableStart, buffer.Slice(88)); } -} \ No newline at end of file +} diff --git a/Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs b/Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs index 32d762fc5..bd58ceec3 100644 --- a/Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs +++ b/Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs @@ -41,7 +41,13 @@ internal class VfsSquashFileSystemReader : VfsReadOnlyFileSystem _metablockCache; - public VfsSquashFileSystemReader(Stream stream) + private readonly StreamCompressorDelegate _decompressor; + + public VfsSquashFileSystemReader(Stream stream) : this(stream, null) + { + } + + public VfsSquashFileSystemReader(Stream stream, SquashFileSystemReaderOptions options) : base(new DiscFileSystemOptions()) { _context = new Context @@ -54,14 +60,28 @@ public VfsSquashFileSystemReader(Stream stream) stream.Position = 0; _context.SuperBlock.ReadFrom(stream, _context.SuperBlock.Size); + var compressionOptions = CompressionOptions.ReadFrom(stream, _context.SuperBlock); + if (_context.SuperBlock.Magic != SuperBlock.SquashFsMagic) { throw new IOException("Invalid SquashFS filesystem - magic mismatch"); } - if (_context.SuperBlock.Compression != SuperBlock.CompressionType.ZLib) + if (_context.SuperBlock.Compression == SquashFileSystemCompressionKind.ZLib) + { + _decompressor = static stream => new ZlibStream(stream, CompressionMode.Decompress, true); + } + + // Let's override the decompressor if the user has provided one + var decompressor = options?.GetDecompressor?.Invoke(_context.SuperBlock.Compression, compressionOptions); + if (decompressor != null) + { + _decompressor = decompressor; + } + + if (_decompressor is null) { - throw new IOException("Unsupported compression used"); + throw new IOException($"Unsupported compression {_context.SuperBlock.Compression} used"); } if (_context.SuperBlock.ExtendedAttrsTableStart != -1) @@ -229,8 +249,7 @@ private Block ReadBlock(long pos, int diskLen) stream.ReadExactly(_ioBuffer, 0, readLen); - using var zlibStream = new ZlibStream(new MemoryStream(_ioBuffer, 0, readLen, false), - CompressionMode.Decompress, true); + using var zlibStream = _decompressor(new MemoryStream(_ioBuffer, 0, readLen, false, true)); block.Available = zlibStream.ReadMaximum(block.Data, 0, (int)_context.SuperBlock.BlockSize); } else @@ -257,11 +276,11 @@ private Metablock ReadMetaBlock(long pos) stream.ReadExactly(buffer); int readLen = EndianUtilities.ToUInt16LittleEndian(buffer); - var isCompressed = (readLen & 0x8000) == 0; - readLen &= 0x7FFF; + var isCompressed = (readLen & Metablock.SQUASHFS_COMPRESSED_BIT) == 0; + readLen &= Metablock.SQUASHFS_COMPRESSED_BIT_SIZE_MASK; if (readLen == 0) { - readLen = 0x8000; + readLen = Metablock.SQUASHFS_COMPRESSED_BIT; } block.NextBlockStart = pos + readLen + 2; @@ -275,8 +294,7 @@ private Metablock ReadMetaBlock(long pos) stream.ReadExactly(_ioBuffer, 0, readLen); - using var zlibStream = new ZlibStream(new MemoryStream(_ioBuffer, 0, readLen, false), - CompressionMode.Decompress, true); + using var zlibStream = _decompressor(new MemoryStream(_ioBuffer, 0, readLen, false, true)); block.Available = zlibStream.ReadMaximum(block.Data, 0, MetadataBufferSize); } else diff --git a/Tests/LibraryTests/LibraryTests.csproj b/Tests/LibraryTests/LibraryTests.csproj index 101ac1d3e..ea913913b 100644 --- a/Tests/LibraryTests/LibraryTests.csproj +++ b/Tests/LibraryTests/LibraryTests.csproj @@ -1,5 +1,6 @@  + net8.0;net48;net46 portable @@ -8,42 +9,47 @@ false Latest ../../SigningKey.snk - true - true + true + true + + - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - + + + @@ -53,16 +59,20 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + PreserveNewest - \ No newline at end of file + + diff --git a/Tests/LibraryTests/SquashFs/SquashFileSystemBuilderTest.cs b/Tests/LibraryTests/SquashFs/SquashFileSystemBuilderTest.cs index fcc4c3067..252aa6cd2 100644 --- a/Tests/LibraryTests/SquashFs/SquashFileSystemBuilderTest.cs +++ b/Tests/LibraryTests/SquashFs/SquashFileSystemBuilderTest.cs @@ -20,10 +20,13 @@ // DEALINGS IN THE SOFTWARE. // +using System; using System.IO; using System.Linq; using DiscUtils; using DiscUtils.SquashFs; +using K4os.Compression.LZ4; + using LibraryTests.Helpers; using Xunit; @@ -178,4 +181,129 @@ public void BlockData() Assert.Equal(testData[i], buffer[i] /*, "Data differs at index " + i*/); } } + + [Fact] + public void BlockDataLz4() + { + var testData = new byte[(512 * 1024) + 21]; + for (var i = 0; i < testData.Length; ++i) + { + testData[i] = (byte)(i % 33); + } + + var fsImage = new MemoryStream(); + + var builder = new SquashFileSystemBuilder(new SquashFileSystemBuilderOptions() + { + + CompressionKind = SquashFileSystemCompressionKind.Lz4, + GetCompressor = (kind, options) => kind == SquashFileSystemCompressionKind.Lz4 ? static stream => new SimpleLz4Stream(stream) : null + }); + + builder.AddFile(@"file", new MemoryStream(testData)); + builder.Build(fsImage); + + var reader = new SquashFileSystemReader(fsImage, new SquashFileSystemReaderOptions() + { + GetDecompressor = (kind, options) => kind == SquashFileSystemCompressionKind.Lz4 ? static stream => new SimpleLz4Stream(stream) : null + }); + + using Stream fs = reader.OpenFile("file", FileMode.Open); + var buffer = new byte[(512 * 1024) + 1024]; + var numRead = fs.Read(buffer, 0, buffer.Length); + + Assert.Equal(testData.Length, numRead); + for (var i = 0; i < testData.Length; ++i) + { + Assert.Equal(testData[i], buffer[i] /*, "Data differs at index " + i*/); + } + } + + /// + /// A simple stream that uses LZ4 to compress and decompress data. + /// Read and Write methods expect the whole buffer to be read or written as we can only rely on LZ4 standard decode/encode buffer methods. + /// + private sealed class SimpleLz4Stream : Stream + { + private MemoryStream _inner; + + public SimpleLz4Stream(MemoryStream inner) + { + _inner = inner; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_inner.Position == _inner.Length) + { + return 0; + } + // This is supposed to be called only once + var srcBuffer = _inner.GetBuffer(); + var decompressed = LZ4Codec.Decode(srcBuffer, (int)_inner.Position, (int)(_inner.Length - _inner.Position), buffer, offset, count); + _inner.Position = _inner.Length; + return decompressed; + } + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.SetLength(count); + var destBuffer = _inner.GetBuffer(); + var compressed = LZ4Codec.Encode(buffer, offset, count, destBuffer, 0, destBuffer.Length); + // If the dest buffer is too small, fake that we wrote the whole buffer + // In that case, the compressed buffer won't be use because it is at least equal to the uncompressed size. + if (compressed < 0) + { + compressed = count; + } + _inner.SetLength(compressed); + _inner.Position = compressed; + } + +#if NET8_0_OR_GREATER + public override int Read(Span buffer) + { + if (_inner.Position == _inner.Length) + { + return 0; + } + var srcBuffer = _inner.ToArray(); + var decompressed = LZ4Codec.Decode(new ReadOnlySpan(srcBuffer, (int)_inner.Position, (int)(_inner.Length - _inner.Position)), buffer); + _inner.Position = _inner.Length; + return decompressed; + } + public override void Write(ReadOnlySpan buffer) + { + _inner.SetLength(buffer.Length); + var destBuffer = _inner.GetBuffer(); + var compressed = LZ4Codec.Encode(buffer, destBuffer); + // If the dest buffer is too small, fake that we wrote the whole buffer + // In that case, the compressed buffer won't be use because it is at least equal to the uncompressed size. + if (compressed < 0) + { + compressed = buffer.Length; + } + _inner.SetLength(compressed); + _inner.Position = compressed; + } +#endif + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => _inner.CanWrite; + + public override long Length => _inner.Length; + + public override long Position { get; set; } + } }