├── assets └── helloworld.zip ├── tools ├── prompt.ps1 ├── requiredModules.psd1 ├── ProjectBuilder │ ├── Types.cs │ ├── Documentation.cs │ ├── ProjectBuilder.csproj │ ├── Extensions.cs │ ├── Project.cs │ ├── Pester.cs │ ├── Module.cs │ └── ProjectInfo.cs ├── PesterTest.ps1 └── InvokeBuild.ps1 ├── src ├── PSCompression │ ├── ArchiveType.cs │ ├── ZipEntryType.cs │ ├── Algorithm.cs │ ├── Records.cs │ ├── Dbg │ │ └── Dbg.cs │ ├── Commands │ │ ├── ExpandTarEntryCommand.cs │ │ ├── ConvertFromGzipStringCommand.cs │ │ ├── ConvertFromDeflateStringCommand.cs │ │ ├── ConvertFromBrotliStringCommand.cs │ │ ├── ConvertToGzipStringCommand.cs │ │ ├── ConvertToZLibStringCommand.cs │ │ ├── ConvertToDeflateStringCommand.cs │ │ ├── ConvertFromZLibStringCommand.cs │ │ ├── ConvertToBrotliStringCommand.cs │ │ ├── ExpandZipEntryCommand.cs │ │ ├── RemoveZipEntryCommand.cs │ │ ├── GetTarEntryContentCommand.cs │ │ ├── GetZipEntryCommand.cs │ │ ├── GetZipEntryContentCommand.cs │ │ ├── GetTarEntryCommand.cs │ │ ├── CompressZipArchiveCommand.cs │ │ ├── CompressTarArchiveCommand.cs │ │ ├── SetZipEntryContentCommand.cs │ │ ├── RenameZipEntryCommand.cs │ │ └── ExpandTarArchiveCommand.cs │ ├── Exceptions │ │ ├── EntryNotFoundException.cs │ │ ├── DuplicatedEntryException.cs │ │ ├── InvalidNameException.cs │ │ └── ExceptionHelper.cs │ ├── Abstractions │ │ ├── EntryStreamOpsBase.cs │ │ ├── EntryBase.cs │ │ ├── GetEntryContentCommandBase.cs │ │ ├── TarEntryBase.cs │ │ ├── FromCompressedStringCommandBase.cs │ │ ├── ExpandEntryCommandBase.cs │ │ ├── ZipEntryBase.IO.Compression.cs │ │ ├── CommandWithPathBase.cs │ │ ├── ToCompressedStringCommandBase.cs │ │ ├── ZipEntryBase.SharpZipLib.Zip.cs │ │ └── GetEntryCommandBase.cs │ ├── TarEntryDirectory.cs │ ├── SortingOps.cs │ ├── AlgorithmMappings.cs │ ├── EntryByteReader.cs │ ├── PSCompression.sln │ ├── ZipEntryDirectory.cs │ ├── ZipArchiveCache.cs │ ├── internal │ │ └── _Format.cs │ ├── ZipEntryByteWriter.cs │ ├── EncodingCompleter.cs │ ├── PSCompression.csproj │ ├── EncodingTransformation.cs │ ├── ZipEntryCache.cs │ ├── Extensions │ │ ├── MiscExtensions.cs │ │ └── PathExtensions.cs │ ├── TarEntryFile.cs │ ├── ZipEntryFile.cs │ ├── OnModuleImportAndRemove.cs │ ├── ZLibStream.cs │ └── ZipEntryMoveCache.cs └── PSCompression.Shared │ ├── PSCompression.Shared.csproj │ ├── SharedUtil.cs │ └── LoadContext.cs ├── .markdownlint.json ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── tasks.json ├── tests ├── DirectoryEntryTypes.tests.ps1 ├── EncodingCompleter.tests.ps1 ├── FormattingInternals.tests.ps1 ├── shared.psm1 ├── EncodingTransformation.tests.ps1 ├── FileEntryTypes.tests.ps1 ├── StringCompressionExpansionCommands.tests.ps1 └── ZipEntryBase.tests.ps1 ├── LICENSE ├── module ├── PSCompression.psm1 └── PSCompression.Format.ps1xml ├── docs └── en-US │ ├── ConvertFrom-BrotliString.md │ ├── ConvertFrom-GzipString.md │ ├── ConvertFrom-DeflateString.md │ ├── Remove-ZipEntry.md │ ├── ConvertFrom-ZLibString.md │ ├── Expand-TarEntry.md │ └── ConvertTo-BrotliString.md ├── .github └── workflows │ └── ci.yml ├── CHANGELOG.md └── .gitignore /assets/helloworld.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santisq/PSCompression/HEAD/assets/helloworld.zip -------------------------------------------------------------------------------- /tools/prompt.ps1: -------------------------------------------------------------------------------- 1 | function prompt { "PS $($PWD.Path -replace '.+(?=\\)', '..')$('>' * ($nestedPromptLevel + 1)) " } 2 | -------------------------------------------------------------------------------- /src/PSCompression/ArchiveType.cs: -------------------------------------------------------------------------------- 1 | namespace PSCompression; 2 | 3 | internal enum ArchiveType 4 | { 5 | zip, 6 | tar 7 | } 8 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryType.cs: -------------------------------------------------------------------------------- 1 | namespace PSCompression; 2 | 3 | public enum EntryType 4 | { 5 | Directory = 0, 6 | Archive = 1 7 | } 8 | -------------------------------------------------------------------------------- /src/PSCompression/Algorithm.cs: -------------------------------------------------------------------------------- 1 | namespace PSCompression; 2 | 3 | public enum Algorithm 4 | { 5 | gz, 6 | bz2, 7 | zst, 8 | lz, 9 | none 10 | } 11 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "no-hard-tabs": true, 4 | "no-duplicate-heading": false, 5 | "line-length": false, 6 | "no-inline-html": false, 7 | "ul-indent": false 8 | } 9 | -------------------------------------------------------------------------------- /tools/requiredModules.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | InvokeBuild = '5.12.1' 3 | platyPS = '0.14.2' 4 | PSScriptAnalyzer = '1.23.0' 5 | Pester = '5.7.1' 6 | PSUsing = '1.0.0' 7 | } 8 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Types.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProjectBuilder; 4 | 5 | public enum Configuration 6 | { 7 | Debug, 8 | Release 9 | } 10 | 11 | internal record struct ModuleDownload(string Module, Version Version); 12 | -------------------------------------------------------------------------------- /src/PSCompression/Records.cs: -------------------------------------------------------------------------------- 1 | using PSCompression.Abstractions; 2 | 3 | namespace PSCompression; 4 | 5 | internal record struct EntryWithPath(ZipEntryBase ZipEntry, string Path); 6 | 7 | internal record struct PathWithType(string Path, EntryType EntryType); 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "formulahendry.dotnet-test-explorer", 6 | "ms-dotnettools.csharp", 7 | "ms-vscode.powershell", 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /src/PSCompression/Dbg/Dbg.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace PSCompression; 5 | 6 | internal static class Dbg 7 | { 8 | [Conditional("DEBUG")] 9 | public static void Assert([DoesNotReturnIf(false)] bool condition) => 10 | Debug.Assert(condition); 11 | } 12 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Documentation.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ProjectBuilder; 4 | 5 | public record struct Documentation(string Source, string Output) 6 | { 7 | public readonly Hashtable GetParams() => new() 8 | { 9 | ["Path"] = Source, 10 | ["OutputPath"] = Output 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/PSCompression.Shared/PSCompression.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | PSCompression.Shared 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/ProjectBuilder.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | enable 6 | latest 7 | ProjectBuilder 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ExpandTarEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Management.Automation; 3 | using PSCompression.Abstractions; 4 | 5 | namespace PSCompression.Commands; 6 | 7 | [Cmdlet(VerbsData.Expand, "TarEntry")] 8 | [OutputType(typeof(FileInfo), typeof(DirectoryInfo))] 9 | [Alias("untarentry")] 10 | public sealed class ExpandTarEntryCommand : ExpandEntryCommandBase 11 | { 12 | protected override FileSystemInfo Extract(TarEntryBase entry) => 13 | entry.ExtractTo(Destination!, Force); 14 | } 15 | -------------------------------------------------------------------------------- /src/PSCompression/Exceptions/EntryNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PSCompression.Exceptions; 4 | 5 | public sealed class EntryNotFoundException : IOException 6 | { 7 | internal string _path; 8 | 9 | private EntryNotFoundException(string message, string path) 10 | : base(message: message) 11 | { 12 | _path = path; 13 | } 14 | 15 | internal static EntryNotFoundException Create(string path, string source) => 16 | new($"Cannot find an entry with path: '{path}' in '{source}'.", path); 17 | } 18 | -------------------------------------------------------------------------------- /src/PSCompression/Exceptions/DuplicatedEntryException.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PSCompression.Exceptions; 4 | 5 | public sealed class DuplicatedEntryException : IOException 6 | { 7 | internal string _path; 8 | 9 | private DuplicatedEntryException(string message, string path) 10 | : base(message: message) 11 | { 12 | _path = path; 13 | } 14 | 15 | internal static DuplicatedEntryException Create(string path, string source) => 16 | new($"An entry with path '{path}' already exists in '{source}'.", path); 17 | } 18 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertFromGzipStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsData.ConvertFrom, "GzipString")] 9 | [OutputType(typeof(string))] 10 | [Alias("fromgzipstring")] 11 | public sealed class ConvertFromGzipStringCommand : FromCompressedStringCommandBase 12 | { 13 | protected override Stream CreateDecompressionStream(Stream inputStream) => 14 | new GZipStream(inputStream, CompressionMode.Decompress); 15 | } 16 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertFromDeflateStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsData.ConvertFrom, "DeflateString")] 9 | [OutputType(typeof(string))] 10 | [Alias("fromdeflatestring")] 11 | public sealed class ConvertFromDeflateStringCommand : FromCompressedStringCommandBase 12 | { 13 | protected override Stream CreateDecompressionStream(Stream inputStream) => 14 | new DeflateStream(inputStream, CompressionMode.Decompress); 15 | } 16 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsData.ConvertFrom, "BrotliString")] 9 | [OutputType(typeof(string))] 10 | [Alias("frombrotlistring")] 11 | public sealed class ConvertFromBrotliStringCommand : FromCompressedStringCommandBase 12 | { 13 | protected override Stream CreateDecompressionStream(Stream inputStream) => 14 | new BrotliSharpLib.BrotliStream(inputStream, CompressionMode.Decompress); 15 | } 16 | -------------------------------------------------------------------------------- /src/PSCompression/Exceptions/InvalidNameException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PSCompression.Exceptions; 4 | 5 | public sealed class InvalidNameException : ArgumentException 6 | { 7 | internal string _name; 8 | 9 | private InvalidNameException(string message, string name) 10 | : base(message: message) 11 | { 12 | _name = name; 13 | } 14 | 15 | internal static InvalidNameException Create(string name) => 16 | new("Cannot rename the specified target, because it represents a path, " 17 | + "device name or contains invalid File Name characters.", name); 18 | } 19 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertToGzipStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsData.ConvertTo, "GzipString")] 9 | [OutputType(typeof(byte[]), typeof(string))] 10 | [Alias("togzipstring")] 11 | public sealed class ConvertToGzipStringCommand : ToCompressedStringCommandBase 12 | { 13 | protected override Stream CreateCompressionStream( 14 | Stream outputStream, 15 | CompressionLevel compressionLevel) 16 | => new GZipStream(outputStream, compressionLevel); 17 | } 18 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertToZLibStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsData.ConvertTo, "ZLibString")] 9 | [OutputType(typeof(byte[]), typeof(string))] 10 | [Alias("tozlibstring")] 11 | public sealed class ConvertToZLibStringCommand : ToCompressedStringCommandBase 12 | { 13 | protected override Stream CreateCompressionStream( 14 | Stream outputStream, 15 | CompressionLevel compressionLevel) 16 | => new ZlibStream(outputStream, compressionLevel); 17 | } 18 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertToDeflateStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsData.ConvertTo, "DeflateString")] 9 | [OutputType(typeof(byte[]), typeof(string))] 10 | [Alias("todeflatestring")] 11 | public sealed class ConvertToDeflateStringCommand : ToCompressedStringCommandBase 12 | { 13 | protected override Stream CreateCompressionStream( 14 | Stream outputStream, 15 | CompressionLevel compressionLevel) 16 | => new DeflateStream(outputStream, compressionLevel); 17 | } 18 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/EntryStreamOpsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PSCompression.Abstractions; 5 | 6 | internal abstract class EntryStreamOpsBase(Stream stream) : IDisposable 7 | { 8 | private bool _disposed; 9 | 10 | protected Stream Stream { get; } = stream; 11 | 12 | protected virtual void Dispose(bool disposing) 13 | { 14 | if (disposing && !_disposed) 15 | { 16 | Stream.Dispose(); 17 | _disposed = true; 18 | } 19 | } 20 | 21 | public void Dispose() 22 | { 23 | Dispose(disposing: true); 24 | GC.SuppressFinalize(this); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertFromZLibStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsData.ConvertFrom, "ZLibString")] 9 | [OutputType(typeof(string))] 10 | [Alias("fromzlibstring")] 11 | public sealed class ConvertFromZLibStringCommand : FromCompressedStringCommandBase 12 | { 13 | protected override Stream CreateDecompressionStream(Stream inputStream) 14 | { 15 | inputStream.Seek(2, SeekOrigin.Begin); 16 | DeflateStream deflate = new(inputStream, CompressionMode.Decompress); 17 | return deflate; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertToBrotliStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | using PSCompression.Extensions; 6 | 7 | namespace PSCompression.Commands; 8 | 9 | [Cmdlet(VerbsData.ConvertTo, "BrotliString")] 10 | [OutputType(typeof(byte[]), typeof(string))] 11 | [Alias("tobrotlistring")] 12 | public sealed class ConvertToBrotliStringCommand : ToCompressedStringCommandBase 13 | { 14 | protected override Stream CreateCompressionStream( 15 | Stream outputStream, 16 | CompressionLevel compressionLevel) 17 | => outputStream.AsBrotliCompressedStream(compressionLevel); 18 | } 19 | -------------------------------------------------------------------------------- /src/PSCompression.Shared/SharedUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Reflection; 5 | using System.Runtime.Loader; 6 | 7 | namespace PSCompression.Shared; 8 | 9 | [ExcludeFromCodeCoverage] 10 | internal sealed class SharedUtil 11 | { 12 | public static void AddAssemblyInfo(Type type, Dictionary data) 13 | { 14 | Assembly asm = type.Assembly; 15 | 16 | data["Assembly"] = new Dictionary() 17 | { 18 | ["Name"] = asm.GetName().FullName, 19 | ["ALC"] = AssemblyLoadContext.GetLoadContext(asm)?.Name, 20 | ["Location"] = asm.Location 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PSCompression/TarEntryDirectory.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using ICSharpCode.SharpZipLib.Tar; 3 | using PSCompression.Abstractions; 4 | using PSCompression.Extensions; 5 | 6 | namespace PSCompression; 7 | 8 | public sealed class TarEntryDirectory : TarEntryBase 9 | { 10 | internal TarEntryDirectory(TarEntry entry, string source) 11 | : base(entry, source) 12 | { 13 | Name = entry.GetDirectoryName(); 14 | } 15 | 16 | internal TarEntryDirectory(TarEntry entry, Stream? stream) 17 | : base(entry, stream) 18 | { 19 | Name = entry.GetDirectoryName(); 20 | } 21 | 22 | public override EntryType Type => EntryType.Directory; 23 | 24 | protected override string GetFormatDirectoryPath() => $"/{RelativePath.NormalizeEntryPath()}"; 25 | } 26 | -------------------------------------------------------------------------------- /src/PSCompression/SortingOps.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using PSCompression.Abstractions; 5 | using PSCompression.Extensions; 6 | 7 | namespace PSCompression; 8 | 9 | internal static class SortingOps 10 | { 11 | private static string? SortByParent(EntryBase entry) => 12 | Path.GetDirectoryName(entry.RelativePath)?.NormalizeEntryPath(); 13 | 14 | private static int SortByLength(EntryBase entry) => 15 | entry.RelativePath.Count(e => e == '/'); 16 | 17 | private static string SortByName(EntryBase entry) => entry.Name!; 18 | 19 | private static EntryType SortByType(EntryBase entry) => entry.Type; 20 | 21 | internal static IEnumerable ToEntrySort( 22 | this IEnumerable entryCollection) 23 | => entryCollection 24 | .OrderBy(SortByParent) 25 | .ThenBy(SortByType) 26 | .ThenBy(SortByLength) 27 | .ThenBy(SortByName); 28 | } 29 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace ProjectBuilder; 5 | 6 | internal static class Extensions 7 | { 8 | internal static void CopyRecursive(this DirectoryInfo source, string? destination) 9 | { 10 | if (destination is null) 11 | { 12 | throw new ArgumentNullException($"Destination path is null.", nameof(destination)); 13 | } 14 | 15 | if (!Directory.Exists(destination)) 16 | { 17 | Directory.CreateDirectory(destination); 18 | } 19 | 20 | foreach (DirectoryInfo dir in source.EnumerateDirectories("*", SearchOption.AllDirectories)) 21 | { 22 | Directory.CreateDirectory(dir.FullName.Replace(source.FullName, destination)); 23 | } 24 | 25 | foreach (FileInfo file in source.EnumerateFiles("*", SearchOption.AllDirectories)) 26 | { 27 | file.CopyTo(file.FullName.Replace(source.FullName, destination)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/DirectoryEntryTypes.tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | 3 | $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 4 | $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 5 | 6 | Import-Module $manifestPath 7 | Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) 8 | 9 | Describe 'Directory Entry Types' { 10 | BeforeAll { 11 | $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 12 | New-ZipEntry $zip.FullName -EntryPath afolder/ 13 | $tarArchive = New-Item (Join-Path $TestDrive afolder) -ItemType Directory -Force | 14 | Compress-TarArchive -Destination 'testTarFile' -PassThru 15 | 16 | $tarArchive | Out-Null 17 | } 18 | 19 | It 'Should be of type Directory' { 20 | ($zip | Get-ZipEntry).Type | Should -BeExactly ([PSCompression.EntryType]::Directory) 21 | ($tarArchive | Get-TarEntry).Type | Should -BeExactly ([PSCompression.EntryType]::Directory) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/EntryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | 5 | namespace PSCompression.Abstractions; 6 | 7 | public abstract class EntryBase(string source) 8 | { 9 | protected Stream? _stream; 10 | 11 | protected string? _formatDirectoryPath; 12 | 13 | internal string? FormatDirectoryPath { get => _formatDirectoryPath ??= GetFormatDirectoryPath(); } 14 | 15 | [MemberNotNullWhen(true, nameof(_stream))] 16 | internal bool FromStream { get => _stream is not null; } 17 | 18 | public string Source { get; } = source; 19 | 20 | public abstract string? Name { get; protected set; } 21 | 22 | public abstract string RelativePath { get; } 23 | 24 | public abstract DateTime LastWriteTime { get; } 25 | 26 | public abstract long Length { get; internal set; } 27 | 28 | public abstract EntryType Type { get; } 29 | 30 | protected abstract string GetFormatDirectoryPath(); 31 | 32 | public override string ToString() => RelativePath; 33 | } 34 | -------------------------------------------------------------------------------- /src/PSCompression/AlgorithmMappings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace PSCompression; 6 | 7 | internal static class AlgorithmMappings 8 | { 9 | private static readonly Dictionary _mappings = new( 10 | StringComparer.InvariantCultureIgnoreCase) 11 | { 12 | // Gzip 13 | [".gz"] = Algorithm.gz, 14 | [".gzip"] = Algorithm.gz, 15 | [".tgz"] = Algorithm.gz, 16 | 17 | // Bzip2 18 | [".bz2"] = Algorithm.bz2, 19 | [".bzip2"] = Algorithm.bz2, 20 | [".tbz2"] = Algorithm.bz2, 21 | [".tbz"] = Algorithm.bz2, 22 | 23 | // Zstandard 24 | [".zst"] = Algorithm.zst, 25 | 26 | // Lzip 27 | [".lz"] = Algorithm.lz, 28 | 29 | // No compression 30 | [".tar"] = Algorithm.none 31 | }; 32 | 33 | internal static Algorithm Parse(string path) => 34 | _mappings.TryGetValue(Path.GetExtension(path), out Algorithm value) ? value : Algorithm.none; 35 | } 36 | -------------------------------------------------------------------------------- /src/PSCompression/EntryByteReader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Management.Automation; 3 | using PSCompression.Abstractions; 4 | 5 | namespace PSCompression; 6 | 7 | internal sealed class EntryByteReader : EntryStreamOpsBase 8 | { 9 | private readonly byte[] _buffer; 10 | 11 | private readonly int _bufferSize; 12 | 13 | internal EntryByteReader(Stream stream, byte[] buffer) 14 | : base(stream) 15 | { 16 | _buffer = buffer; 17 | _bufferSize = buffer.Length; 18 | } 19 | 20 | internal void StreamBytes(PSCmdlet cmdlet) 21 | { 22 | int bytes; 23 | while ((bytes = Stream.Read(_buffer, 0, _bufferSize)) > 0) 24 | { 25 | for (int i = 0; i < bytes; i++) 26 | { 27 | cmdlet.WriteObject(_buffer[i]); 28 | } 29 | } 30 | } 31 | 32 | internal void ReadAllBytes(PSCmdlet cmdlet) 33 | { 34 | using MemoryStream mem = new(); 35 | Stream.CopyTo(mem); 36 | cmdlet.WriteObject(mem.ToArray()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tools/PesterTest.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter(Mandatory)] 4 | [String] $TestPath, 5 | 6 | [Parameter(Mandatory)] 7 | [String] $OutputFile 8 | ) 9 | 10 | $ErrorActionPreference = 'Stop' 11 | 12 | Get-ChildItem ([IO.Path]::Combine($PSScriptRoot, 'Modules')) -Directory | 13 | Import-Module -Name { $_.FullName } -Force -DisableNameChecking 14 | 15 | [PSCustomObject] $PSVersionTable | Select-Object *, @{ 16 | Name = 'Architecture' 17 | Expression = { 18 | switch ([IntPtr]::Size) { 19 | 4 { 'x86' } 20 | 8 { 'x64' } 21 | default { 'Unknown' } 22 | } 23 | } 24 | } | Format-List | Out-Host 25 | 26 | $configuration = [PesterConfiguration]::Default 27 | $configuration.Output.Verbosity = 'Detailed' 28 | $configuration.Run.Path = $TestPath 29 | $configuration.Run.Throw = $true 30 | $configuration.TestResult.Enabled = $true 31 | $configuration.TestResult.OutputPath = $OutputFile 32 | $configuration.TestResult.OutputFormat = 'NUnitXml' 33 | 34 | Invoke-Pester -Configuration $configuration -WarningAction Ignore 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Santiago Squarzon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /module/PSCompression.psm1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | using namespace System.Reflection 3 | using namespace PSCompression.Shared 4 | 5 | $moduleName = [Path]::GetFileNameWithoutExtension($PSCommandPath) 6 | $frame = 'net8.0' 7 | 8 | if (-not $IsCoreCLR) { 9 | $frame = 'netstandard2.0' 10 | $asm = [Path]::Combine($PSScriptRoot, 'bin', $frame, "${moduleName}.dll") 11 | Import-Module -Name $asm -ErrorAction Stop -PassThru 12 | return 13 | } 14 | 15 | $context = [Path]::Combine($PSScriptRoot, 'bin', $frame, "${moduleName}.Shared.dll") 16 | $isReload = $true 17 | 18 | if (-not ("${moduleName}.Shared.LoadContext" -as [type])) { 19 | $isReload = $false 20 | Add-Type -Path $context 21 | } 22 | 23 | $mainModule = [LoadContext]::Initialize() 24 | $innerMod = Import-Module -Assembly $mainModule -PassThru:$isReload 25 | 26 | if ($innerMod) { 27 | $addExportedCmdlet = [psmoduleinfo].GetMethod( 28 | 'AddExportedCmdlet', [BindingFlags] 'Instance, NonPublic') 29 | 30 | foreach ($cmd in $innerMod.ExportedCmdlets.Values) { 31 | $addExportedCmdlet.Invoke($ExecutionContext.SessionState.Module, @($cmd)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PSCompression/PSCompression.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.001.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSCompression", "PSCompression.csproj", "{04592EEF-913E-46C1-A7C2-20143FA08138}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {04592EEF-913E-46C1-A7C2-20143FA08138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {04592EEF-913E-46C1-A7C2-20143FA08138}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {04592EEF-913E-46C1-A7C2-20143FA08138}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {04592EEF-913E-46C1-A7C2-20143FA08138}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {761C4EBF-BC3E-404E-974F-5DC394531F3C} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryDirectory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using ICSharpCode.SharpZipLib.Zip; 7 | using PSCompression.Abstractions; 8 | using PSCompression.Extensions; 9 | 10 | namespace PSCompression; 11 | 12 | public sealed class ZipEntryDirectory : ZipEntryBase 13 | { 14 | private const StringComparison Comparer = StringComparison.InvariantCultureIgnoreCase; 15 | 16 | public override EntryType Type => EntryType.Directory; 17 | 18 | internal ZipEntryDirectory(ZipEntry entry, string source) 19 | : base(entry, source) 20 | { 21 | Name = entry.GetDirectoryName(); 22 | } 23 | 24 | internal ZipEntryDirectory(ZipEntry entry, Stream? stream) 25 | : base(entry, stream) 26 | { 27 | Name = entry.GetDirectoryName(); 28 | } 29 | 30 | internal IEnumerable GetChilds(ZipArchive zip) => 31 | zip.Entries.Where(e => 32 | !string.Equals(e.FullName, RelativePath, Comparer) 33 | && e.FullName.StartsWith(RelativePath, Comparer)); 34 | 35 | protected override string GetFormatDirectoryPath() => 36 | $"/{RelativePath.NormalizeEntryPath()}"; 37 | } 38 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/GetEntryContentCommandBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Management.Automation; 3 | using System.Text; 4 | 5 | namespace PSCompression.Abstractions; 6 | 7 | [EditorBrowsable(EditorBrowsableState.Never)] 8 | public abstract class GetEntryContentCommandBase : PSCmdlet 9 | where T : EntryBase 10 | { 11 | protected byte[]? Buffer { get; set; } 12 | 13 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 14 | public T[] Entry { get; set; } = null!; 15 | 16 | [Parameter(ParameterSetName = "Stream")] 17 | [ArgumentCompleter(typeof(EncodingCompleter))] 18 | [EncodingTransformation] 19 | [ValidateNotNullOrEmpty] 20 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 21 | 22 | [Parameter] 23 | public SwitchParameter Raw { get; set; } 24 | 25 | [Parameter(ParameterSetName = "Bytes")] 26 | public SwitchParameter AsByteStream { get; set; } 27 | 28 | [Parameter(ParameterSetName = "Bytes")] 29 | [ValidateNotNullOrEmpty] 30 | public int BufferSize { get; set; } = 128_000; 31 | 32 | protected override void BeginProcessing() 33 | { 34 | if (ParameterSetName == "Bytes") 35 | { 36 | Buffer = new byte[BufferSize]; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ExpandZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using System.Security; 5 | using ICSharpCode.SharpZipLib.Zip; 6 | using PSCompression.Abstractions; 7 | using PSCompression.Extensions; 8 | 9 | namespace PSCompression.Commands; 10 | 11 | [Cmdlet(VerbsData.Expand, "ZipEntry")] 12 | [OutputType(typeof(FileInfo), typeof(DirectoryInfo))] 13 | [Alias("unzipentry")] 14 | public sealed class ExpandZipEntryCommand : ExpandEntryCommandBase, IDisposable 15 | { 16 | [Parameter] 17 | public SecureString? Password { get; set; } 18 | 19 | private ZipArchiveCache? _cache; 20 | 21 | protected override FileSystemInfo Extract(ZipEntryBase entry) 22 | { 23 | _cache ??= new ZipArchiveCache(entry => entry.OpenRead(Password)); 24 | ZipFile zip = _cache.GetOrCreate(entry); 25 | 26 | if (entry.IsEncrypted && Password is null && entry is ZipEntryFile fileEntry) 27 | { 28 | zip.Password = fileEntry.PromptForPassword(Host); 29 | } 30 | 31 | return entry.ExtractTo(Destination!, Force, zip); 32 | } 33 | 34 | public void Dispose() 35 | { 36 | _cache?.Dispose(); 37 | GC.SuppressFinalize(this); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PSCompression/ZipArchiveCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PSCompression.Abstractions; 4 | 5 | namespace PSCompression; 6 | 7 | internal sealed class ZipArchiveCache : IDisposable 8 | where TArchive : IDisposable 9 | { 10 | private readonly Dictionary _cache; 11 | 12 | private readonly Func _factory; 13 | 14 | internal ZipArchiveCache(Func factory) 15 | { 16 | _cache = new(StringComparer.OrdinalIgnoreCase); 17 | _factory = factory; 18 | } 19 | 20 | internal TArchive this[string source] => _cache[source]; 21 | 22 | internal void TryAdd(ZipEntryBase entry) 23 | { 24 | if (!_cache.ContainsKey(entry.Source)) 25 | { 26 | _cache[entry.Source] = _factory(entry); 27 | } 28 | } 29 | 30 | internal TArchive GetOrCreate(ZipEntryBase entry) 31 | { 32 | if (!_cache.ContainsKey(entry.Source)) 33 | { 34 | _cache[entry.Source] = _factory(entry); 35 | } 36 | 37 | return _cache[entry.Source]; 38 | } 39 | 40 | public void Dispose() 41 | { 42 | foreach (TArchive zip in _cache.Values) 43 | { 44 | zip?.Dispose(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enableFiletypes": [ 3 | "!powershell" 4 | ], 5 | //-------- Files configuration -------- 6 | // When enabled, will trim trailing whitespace when you save a file. 7 | "files.trimTrailingWhitespace": true, 8 | // When enabled, insert a final new line at the end of the file when saving it. 9 | "files.insertFinalNewline": true, 10 | "search.exclude": { 11 | "Release": true, 12 | "tools/ResGen": true, 13 | "tools/dotnet": true, 14 | }, 15 | "json.schemas": [ 16 | { 17 | "fileMatch": [ 18 | "/test.settings.json" 19 | ], 20 | "url": "./tests/settings.schema.json" 21 | } 22 | ], 23 | "dotnet-test-explorer.testProjectPath": "tests/units/*.csproj", 24 | "editor.rulers": [ 25 | 120, 26 | ], 27 | //-------- PowerShell configuration -------- 28 | // Binary modules cannot be unloaded so running in separate processes solves that problem 29 | //"powershell.debugging.createTemporaryIntegratedConsole": true, 30 | // We use Pester v5 so we don't need the legacy code lens 31 | "powershell.pester.useLegacyCodeLens": false, 32 | "cSpell.words": [ 33 | "pwsh" 34 | ], 35 | "dotnet.defaultSolution": "src\\PSCompression\\PSCompression.sln", 36 | "pester.autoRunOnSave": false, 37 | } 38 | -------------------------------------------------------------------------------- /src/PSCompression/internal/_Format.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | using System.Management.Automation; 5 | using PSCompression.Abstractions; 6 | 7 | namespace PSCompression.Internal; 8 | 9 | #pragma warning disable IDE1006 10 | 11 | [EditorBrowsable(EditorBrowsableState.Never)] 12 | public static class _Format 13 | { 14 | private static readonly CultureInfo _culture = CultureInfo.CurrentCulture; 15 | 16 | private readonly static string[] s_suffix = 17 | [ 18 | "B", 19 | "KB", 20 | "MB", 21 | "GB", 22 | "TB", 23 | "PB", 24 | "EB", 25 | "ZB", 26 | "YB" 27 | ]; 28 | 29 | [Hidden, EditorBrowsable(EditorBrowsableState.Never)] 30 | public static string? GetDirectoryPath(EntryBase entry) => entry.FormatDirectoryPath; 31 | 32 | [Hidden, EditorBrowsable(EditorBrowsableState.Never)] 33 | public static string GetFormattedDate(DateTime dateTime) => 34 | string.Format(_culture, "{0,10:d} {0,8:t}", dateTime); 35 | 36 | [Hidden, EditorBrowsable(EditorBrowsableState.Never)] 37 | public static string GetFormattedLength(long length) 38 | { 39 | int index = 0; 40 | double len = length; 41 | 42 | while (len >= 1024) 43 | { 44 | len /= 1024; 45 | index++; 46 | } 47 | 48 | return $"{Math.Round(len, 2):0.00} {s_suffix[index],2}"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryByteWriter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using PSCompression.Abstractions; 3 | 4 | namespace PSCompression; 5 | 6 | internal sealed class ZipEntryByteWriter : EntryStreamOpsBase 7 | { 8 | private readonly byte[] _buffer; 9 | 10 | private readonly int _bufferSize; 11 | 12 | private int _index; 13 | 14 | internal ZipEntryByteWriter(Stream stream, int bufferSize, bool append = false) 15 | : base(stream) 16 | { 17 | _buffer = new byte[bufferSize]; 18 | _bufferSize = bufferSize; 19 | 20 | if (append) 21 | { 22 | Stream.Seek(0, SeekOrigin.End); 23 | return; 24 | } 25 | 26 | Stream.SetLength(0); 27 | } 28 | 29 | internal void WriteBytes(byte[] bytes) 30 | { 31 | foreach (byte b in bytes) 32 | { 33 | if (_index == _bufferSize) 34 | { 35 | Stream.Write(_buffer, 0, _index); 36 | _index = 0; 37 | } 38 | 39 | _buffer[_index++] = b; 40 | } 41 | } 42 | 43 | private void Flush() 44 | { 45 | if (_index > 0) 46 | { 47 | Stream.Write(_buffer, 0, _index); 48 | Stream.Flush(); 49 | } 50 | } 51 | 52 | protected override void Dispose(bool disposing) 53 | { 54 | if (disposing) 55 | { 56 | Flush(); 57 | } 58 | 59 | base.Dispose(disposing); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/PSCompression/EncodingCompleter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Management.Automation; 4 | using System.Management.Automation.Language; 5 | using System; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace PSCompression; 9 | 10 | public sealed class EncodingCompleter : IArgumentCompleter 11 | { 12 | private static readonly string[] s_encodingSet; 13 | 14 | static EncodingCompleter() 15 | { 16 | List set = new([ 17 | "ascii", 18 | "bigendianUtf32", 19 | "unicode", 20 | "utf8", 21 | "utf8NoBOM", 22 | "bigendianUnicode", 23 | "oem", 24 | "utf8BOM", 25 | "utf32" 26 | ]); 27 | 28 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 29 | { 30 | set.Add("ansi"); 31 | } 32 | 33 | s_encodingSet = [.. set]; 34 | } 35 | 36 | public IEnumerable CompleteArgument( 37 | string commandName, 38 | string parameterName, 39 | string wordToComplete, 40 | CommandAst commandAst, 41 | IDictionary fakeBoundParameters) 42 | { 43 | foreach (string encoding in s_encodingSet) 44 | { 45 | if (encoding.StartsWith(wordToComplete, StringComparison.InvariantCultureIgnoreCase)) 46 | { 47 | yield return new CompletionResult(encoding); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/TarEntryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using ICSharpCode.SharpZipLib.Tar; 4 | using PSCompression.Extensions; 5 | 6 | namespace PSCompression.Abstractions; 7 | 8 | public abstract class TarEntryBase(TarEntry entry, string source) : EntryBase(source) 9 | { 10 | public override string? Name { get; protected set; } 11 | 12 | public override string RelativePath { get; } = entry.Name; 13 | 14 | public override DateTime LastWriteTime { get; } = entry.ModTime; 15 | 16 | public override long Length { get; internal set; } = entry.Size; 17 | 18 | protected TarEntryBase(TarEntry entry, Stream? stream) 19 | : this(entry, $"InputStream.{Guid.NewGuid()}") 20 | { 21 | _stream = stream; 22 | } 23 | 24 | internal FileSystemInfo ExtractTo( 25 | string destination, 26 | bool overwrite) 27 | { 28 | destination = Path.GetFullPath( 29 | Path.Combine(destination, RelativePath)); 30 | 31 | if (this is not TarEntryFile entryFile) 32 | { 33 | DirectoryInfo dir = new(destination); 34 | dir.Create(overwrite); 35 | return dir; 36 | } 37 | 38 | FileInfo file = new(destination); 39 | file.Directory!.Create(); 40 | 41 | using FileStream destStream = file.Open( 42 | overwrite ? FileMode.Create : FileMode.CreateNew, 43 | FileAccess.Write); 44 | 45 | entryFile.GetContentStream(destStream); 46 | return file; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PSCompression/PSCompression.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net8.0 5 | enable 6 | true 7 | PSCompression 8 | latest 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | true 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "PowerShell launch", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "program": "pwsh", 12 | "args": [ 13 | "-NoExit", 14 | "-NoProfile", 15 | "-Command", 16 | ". ./tools/prompt.ps1;", 17 | "Import-Module ./output/PSCompression" 18 | ], 19 | "cwd": "${workspaceFolder}", 20 | "stopAtEntry": false, 21 | "console": "externalTerminal", 22 | }, 23 | { 24 | "name": "PowerShell Launch Current File", 25 | "type": "PowerShell", 26 | "request": "launch", 27 | "script": "${file}", 28 | "cwd": "${workspaceFolder}" 29 | }, 30 | { 31 | "name": ".NET FullCLR Attach", 32 | "type": "clr", 33 | "request": "attach", 34 | "processId": "${command:pickProcess}", 35 | "justMyCode": true, 36 | }, 37 | { 38 | "name": ".NET CoreCLR Attach", 39 | "type": "coreclr", 40 | "request": "attach", 41 | "processId": "${command:pickProcess}", 42 | "justMyCode": true, 43 | }, 44 | ], 45 | } 46 | -------------------------------------------------------------------------------- /tests/EncodingCompleter.tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | 3 | $ErrorActionPreference = 'Stop' 4 | 5 | $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 6 | $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 7 | 8 | Import-Module $manifestPath 9 | Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) 10 | 11 | Describe 'EncodingCompleter Class' { 12 | BeforeAll { 13 | $encodingSet = @( 14 | 'ascii' 15 | 'bigendianUtf32' 16 | 'unicode' 17 | 'utf8' 18 | 'utf8NoBOM' 19 | 'bigendianUnicode' 20 | 'oem' 21 | 'utf8BOM' 22 | 'utf32' 23 | 24 | if ($osIsWindows) { 25 | 'ansi' 26 | } 27 | ) 28 | 29 | $encodingSet | Out-Null 30 | } 31 | 32 | It 'Completes results from a completion set' { 33 | (Complete 'Test-Completer ').CompletionText | 34 | Should -BeExactly $encodingSet 35 | } 36 | 37 | It 'Completes results from a word to complete' { 38 | (Complete 'Test-Completer utf').CompletionText | 39 | Should -BeExactly ($encodingSet -match '^utf') 40 | } 41 | 42 | It 'Should not offer ansi as a completion result if the OS is not Windows' { 43 | if ($osIsWindows) { 44 | (Complete 'Test-Completer ansi').CompletionText | Should -Not -BeNullOrEmpty 45 | return 46 | } 47 | 48 | (Complete 'Test-Completer ansi').CompletionText | Should -BeNullOrEmpty 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "pwsh", 9 | "type": "shell", 10 | "args": [ 11 | "-File", 12 | "${workspaceFolder}/build.ps1" 13 | ], 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | }, 18 | "problemMatcher": "$msCompile" 19 | }, 20 | { 21 | "label": "update docs", 22 | "command": "pwsh", 23 | "type": "shell", 24 | "args": [ 25 | "-Command", 26 | "Import-Module ${workspaceFolder}/output/PSCompression; Import-Module ${workspaceFolder}/tools/Modules/platyPS; Update-MarkdownHelpModule ${workspaceFolder}/docs/en-US -AlphabeticParamsOrder -RefreshModulePage -UpdateInputOutput" 27 | ], 28 | "problemMatcher": [], 29 | "dependsOn": [ 30 | "build" 31 | ] 32 | }, 33 | { 34 | "label": "test", 35 | "command": "pwsh", 36 | "type": "shell", 37 | "args": [ 38 | "-File", 39 | "${workspaceFolder}/build.ps1", 40 | "-Task", 41 | "Test" 42 | ], 43 | "problemMatcher": [], 44 | "dependsOn": [ 45 | "build" 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/RemoveZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | using PSCompression.Exceptions; 6 | 7 | namespace PSCompression.Commands; 8 | 9 | [Cmdlet(VerbsCommon.Remove, "ZipEntry", SupportsShouldProcess = true)] 10 | [OutputType(typeof(void))] 11 | [Alias("ziprm")] 12 | public sealed class RemoveZipEntryCommand : PSCmdlet, IDisposable 13 | { 14 | private readonly ZipArchiveCache _cache = new( 15 | entry => entry.OpenZip(ZipArchiveMode.Update)); 16 | 17 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 18 | public ZipEntryBase[] InputObject { get; set; } = null!; 19 | 20 | protected override void ProcessRecord() 21 | { 22 | foreach (ZipEntryBase entry in InputObject) 23 | { 24 | try 25 | { 26 | if (ShouldProcess(target: entry.ToString(), action: "Remove")) 27 | { 28 | entry.Remove(_cache.GetOrCreate(entry)); 29 | } 30 | } 31 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 32 | { 33 | throw; 34 | } 35 | catch (NotSupportedException exception) 36 | { 37 | ThrowTerminatingError(exception.ToStreamOpenError(entry)); 38 | } 39 | catch (Exception exception) 40 | { 41 | WriteError(exception.ToOpenError(entry.Source)); 42 | } 43 | } 44 | } 45 | 46 | public void Dispose() 47 | { 48 | _cache?.Dispose(); 49 | GC.SuppressFinalize(this); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/PSCompression/EncodingTransformation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace PSCompression; 7 | 8 | public sealed class EncodingTransformation : ArgumentTransformationAttribute 9 | { 10 | public override object Transform( 11 | EngineIntrinsics engineIntrinsics, 12 | object inputData) 13 | { 14 | inputData = inputData is PSObject pso 15 | ? pso.BaseObject 16 | : inputData; 17 | 18 | return inputData switch 19 | { 20 | Encoding enc => enc, 21 | int num => Encoding.GetEncoding(num), 22 | string str => ParseStringEncoding(str), 23 | _ => throw new ArgumentTransformationMetadataException( 24 | $"Could not convert input '{inputData}' to a valid Encoding object."), 25 | }; 26 | } 27 | 28 | private Encoding ParseStringEncoding(string str) => 29 | str.ToLowerInvariant() switch 30 | { 31 | "ascii" => new ASCIIEncoding(), 32 | "bigendianunicode" => new UnicodeEncoding(true, true), 33 | "bigendianutf32" => new UTF32Encoding(true, true), 34 | "oem" => Console.OutputEncoding, 35 | "unicode" => new UnicodeEncoding(), 36 | "utf8" => new UTF8Encoding(false), 37 | "utf8bom" => new UTF8Encoding(true), 38 | "utf8nobom" => new UTF8Encoding(false), 39 | "utf32" => new UTF32Encoding(), 40 | "ansi" => Encoding.GetEncoding(GetACP()), 41 | _ => Encoding.GetEncoding(str), 42 | }; 43 | 44 | [DllImport("Kernel32.dll")] 45 | private static extern int GetACP(); 46 | } 47 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ICSharpCode.SharpZipLib.Zip; 4 | using PSCompression.Abstractions; 5 | using PSCompression.Extensions; 6 | 7 | namespace PSCompression; 8 | 9 | public sealed class ZipEntryCache 10 | { 11 | private readonly Dictionary> _cache; 12 | 13 | internal ZipEntryCache() => _cache = new(StringComparer.InvariantCultureIgnoreCase); 14 | 15 | internal List WithSource(string source) 16 | { 17 | if (!_cache.ContainsKey(source)) 18 | { 19 | _cache[source] = []; 20 | } 21 | 22 | return _cache[source]; 23 | } 24 | 25 | internal void Add(string source, PathWithType pathWithType) => 26 | WithSource(source).Add(pathWithType); 27 | 28 | internal ZipEntryCache AddRange(IEnumerable<(string, PathWithType)> values) 29 | { 30 | foreach ((string source, PathWithType pathWithType) in values) 31 | { 32 | Add(source, pathWithType); 33 | } 34 | 35 | return this; 36 | } 37 | 38 | internal IEnumerable GetEntries() 39 | { 40 | foreach (var entry in _cache) 41 | { 42 | using ZipFile zip = new(entry.Key); 43 | foreach ((string path, EntryType type) in entry.Value) 44 | { 45 | if (!zip.TryGetEntry(path, out ZipEntry? zipEntry)) 46 | { 47 | continue; 48 | } 49 | 50 | if (type == EntryType.Archive) 51 | { 52 | yield return new ZipEntryFile(zipEntry, entry.Key); 53 | continue; 54 | } 55 | 56 | yield return new ZipEntryDirectory(zipEntry, entry.Key); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/FormattingInternals.tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | 3 | $ErrorActionPreference = 'Stop' 4 | 5 | $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 6 | $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 7 | 8 | Import-Module $manifestPath 9 | Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) 10 | 11 | Describe 'Formatting internals' { 12 | BeforeAll { 13 | $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 14 | 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt 15 | New-ZipEntry $zip.FullName -EntryPath afolder/ 16 | $testTarName = 'formattingTarTest' 17 | $testTarpath = Join-Path $TestDrive $testTarName 18 | Get-Structure | Build-Structure $testTarpath 19 | $tarArchive = Compress-TarArchive $testTarpath -Destination $testTarName -PassThru 20 | $tarArchive | Out-Null 21 | } 22 | 23 | It 'Converts Length to their friendly representation' { 24 | [PSCompression.Internal._Format]::GetFormattedLength(1mb) | 25 | Should -BeExactly '1.00 MB' 26 | } 27 | 28 | It 'Gets the directory of an entry' { 29 | $zip | Get-ZipEntry | ForEach-Object { 30 | [PSCompression.Internal._Format]::GetDirectoryPath($_) 31 | } | Should -BeOfType ([string]) 32 | 33 | $tarArchive | Get-TarEntry | ForEach-Object { 34 | [PSCompression.Internal._Format]::GetDirectoryPath($_) 35 | } | Should -BeOfType ([string]) 36 | } 37 | 38 | It 'Formats datetime instances' { 39 | [PSCompression.Internal._Format]::GetFormattedDate([datetime]::Now) | 40 | Should -BeExactly ([string]::Format( 41 | [CultureInfo]::CurrentCulture, 42 | '{0,10:d} {0,8:t}', 43 | [datetime]::Now)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/GetTarEntryContentCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using PSCompression.Abstractions; 5 | using PSCompression.Exceptions; 6 | using PSCompression.Extensions; 7 | 8 | namespace PSCompression.Commands; 9 | 10 | [Cmdlet(VerbsCommon.Get, "TarEntryContent", DefaultParameterSetName = "Stream")] 11 | [OutputType(typeof(string), ParameterSetName = ["Stream"])] 12 | [OutputType(typeof(byte), ParameterSetName = ["Bytes"])] 13 | [Alias("targec")] 14 | public sealed class GetTarEntryContentCommand : GetEntryContentCommandBase 15 | { 16 | protected override void ProcessRecord() 17 | { 18 | foreach (TarEntryFile entry in Entry) 19 | { 20 | try 21 | { 22 | ReadEntry(entry); 23 | } 24 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 25 | { 26 | throw; 27 | } 28 | catch (Exception exception) 29 | { 30 | WriteError(exception.ToOpenError(entry.Source)); 31 | } 32 | } 33 | } 34 | 35 | private void ReadEntry(TarEntryFile entry) 36 | { 37 | using MemoryStream mem = new(); 38 | if (!entry.GetContentStream(mem)) 39 | { 40 | return; 41 | } 42 | 43 | if (AsByteStream) 44 | { 45 | if (Raw) 46 | { 47 | WriteObject(mem.ToArray()); 48 | return; 49 | } 50 | 51 | using EntryByteReader byteReader = new(mem, Buffer!); 52 | byteReader.StreamBytes(this); 53 | return; 54 | } 55 | 56 | using StreamReader reader = new(mem, Encoding); 57 | if (Raw) 58 | { 59 | reader.ReadToEnd(this); 60 | return; 61 | } 62 | 63 | reader.ReadLines(this); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/PSCompression/Extensions/MiscExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | using System.Management.Automation; 5 | using System.Management.Automation.Host; 6 | using System.Net; 7 | using System.Security; 8 | 9 | namespace PSCompression.Extensions; 10 | 11 | internal static class MiscExtensions 12 | { 13 | internal static void Deconstruct( 14 | this KeyValuePair keyv, 15 | out TKey key, 16 | out TValue value) 17 | { 18 | key = keyv.Key; 19 | value = keyv.Value; 20 | } 21 | 22 | internal static string AsPlainText(this SecureString secureString) => 23 | new NetworkCredential(string.Empty, secureString).Password; 24 | 25 | [ExcludeFromCodeCoverage] 26 | internal static string PromptForPassword(this ZipEntryFile entry, PSHost host) 27 | { 28 | host.UI.Write( 29 | $"Encrypted entry '{entry.RelativePath}' in '{entry.Source}' requires a password.\n" + 30 | "Tip: Use -Password to avoid this prompt in the future.\n" + 31 | "Enter password: "); 32 | 33 | return host.UI.ReadLineAsSecureString().AsPlainText(); 34 | } 35 | 36 | internal static void ReadToEnd(this StreamReader reader, PSCmdlet cmdlet) 37 | => cmdlet.WriteObject(reader.ReadToEnd()); 38 | 39 | internal static void ReadLines(this StreamReader reader, PSCmdlet cmdlet) 40 | { 41 | string? line; 42 | while ((line = reader.ReadLine()) is not null) 43 | { 44 | cmdlet.WriteObject(line); 45 | } 46 | } 47 | 48 | internal static void WriteLines(this StreamWriter writer, string[] lines) 49 | { 50 | foreach (string line in lines) 51 | { 52 | writer.WriteLine(line); 53 | } 54 | } 55 | 56 | internal static void WriteContent(this StreamWriter writer, string[] lines) 57 | { 58 | foreach (string line in lines) 59 | { 60 | writer.Write(line); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/shared.psm1: -------------------------------------------------------------------------------- 1 | using namespace System.Management.Automation 2 | using namespace System.Runtime.InteropServices 3 | using namespace PSCompression 4 | 5 | function Complete { 6 | param([string] $Expression) 7 | 8 | [CommandCompletion]::CompleteInput($Expression, $Expression.Length, $null).CompletionMatches 9 | } 10 | 11 | function Test-Completer { 12 | param( 13 | [ArgumentCompleter([EncodingCompleter])] 14 | [string] $Test 15 | ) 16 | } 17 | 18 | function Get-Structure { 19 | foreach ($folder in 0..5) { 20 | $folder = 'testfolder{0:D2}/' -f $folder 21 | $folder 22 | foreach ($file in 0..5) { 23 | [System.IO.Path]::Combine($folder, 'testfile{0:D2}.txt' -f $file) 24 | } 25 | } 26 | } 27 | 28 | function Build-Structure { 29 | param( 30 | [Parameter(ValueFromPipeline, Mandatory)] 31 | [string] $Item, 32 | 33 | [Parameter(Mandatory, Position = 0)] 34 | [string] $Path) 35 | 36 | begin { 37 | $fileCount = $dirCount = 0 38 | } 39 | process { 40 | $isFile = $Item.EndsWith('.txt') 41 | 42 | $newItemSplat = @{ 43 | ItemType = ('Directory', 'File')[$isFile] 44 | Value = Get-Random 45 | Force = $true 46 | Path = Join-Path $Path $Item 47 | } 48 | 49 | $null = New-Item @newItemSplat 50 | 51 | if ($isFile) { 52 | $fileCount++ 53 | return 54 | } 55 | 56 | $dirCount++ 57 | } 58 | end { 59 | $dirCount++ # Includes the folder itself 60 | 61 | [pscustomobject]@{ 62 | File = $fileCount 63 | Directory = $dirCount 64 | } 65 | } 66 | } 67 | 68 | $osIsWindows = [RuntimeInformation]::IsOSPlatform([OSPlatform]::Windows) 69 | $osIsWindows | Out-Null 70 | 71 | $exportModuleMemberSplat = @{ 72 | Variable = 'moduleName', 'manifestPath', 'osIsWindows' 73 | Function = 'Decode', 'Complete', 'Test-Completer', 'Get-Structure', 'Build-Structure' 74 | } 75 | 76 | Export-ModuleMember @exportModuleMemberSplat 77 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/FromCompressedStringCommandBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | using System.Management.Automation; 5 | using System.Text; 6 | using PSCompression.Exceptions; 7 | using PSCompression.Extensions; 8 | 9 | namespace PSCompression.Abstractions; 10 | 11 | [EditorBrowsable(EditorBrowsableState.Never)] 12 | public abstract class FromCompressedStringCommandBase : PSCmdlet 13 | { 14 | protected delegate Stream DecompressionStreamFactory(Stream inputStream); 15 | 16 | [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] 17 | public string[] InputObject { get; set; } = null!; 18 | 19 | [Parameter] 20 | [ArgumentCompleter(typeof(EncodingCompleter))] 21 | [EncodingTransformation] 22 | [ValidateNotNullOrEmpty] 23 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 24 | 25 | [Parameter] 26 | public SwitchParameter Raw { get; set; } 27 | 28 | private void Decompress( 29 | string base64string, 30 | DecompressionStreamFactory decompressionStreamFactory) 31 | { 32 | using MemoryStream inStream = new(Convert.FromBase64String(base64string)); 33 | using Stream decompressStream = decompressionStreamFactory(inStream); 34 | using StreamReader reader = new(decompressStream, Encoding); 35 | 36 | if (Raw) 37 | { 38 | reader.ReadToEnd(this); 39 | return; 40 | } 41 | 42 | reader.ReadLines(this); 43 | } 44 | 45 | protected abstract Stream CreateDecompressionStream(Stream inputStream); 46 | 47 | protected override void ProcessRecord() 48 | { 49 | foreach (string line in InputObject) 50 | { 51 | try 52 | { 53 | Decompress(line, CreateDecompressionStream); 54 | } 55 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 56 | { 57 | throw; 58 | } 59 | catch (Exception exception) 60 | { 61 | WriteError(exception.ToEnumerationError(line)); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/GetZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Management.Automation; 3 | using System.IO; 4 | using PSCompression.Abstractions; 5 | using ICSharpCode.SharpZipLib.Zip; 6 | 7 | namespace PSCompression.Commands; 8 | 9 | [Cmdlet(VerbsCommon.Get, "ZipEntry", DefaultParameterSetName = "Path")] 10 | [OutputType(typeof(ZipEntryDirectory), typeof(ZipEntryFile))] 11 | [Alias("zipge")] 12 | public sealed class GetZipEntryCommand : GetEntryCommandBase 13 | { 14 | internal override ArchiveType ArchiveType => ArchiveType.zip; 15 | 16 | protected override IEnumerable GetEntriesFromFile(string path) 17 | { 18 | List entries = []; 19 | using (ZipFile zip = new(path)) 20 | { 21 | foreach (ZipEntry entry in zip) 22 | { 23 | if (ShouldSkipEntry(entry.IsDirectory)) 24 | { 25 | continue; 26 | } 27 | 28 | if (!ShouldInclude(entry.Name) || ShouldExclude(entry.Name)) 29 | { 30 | continue; 31 | } 32 | 33 | entries.Add(entry.IsDirectory 34 | ? new ZipEntryDirectory(entry, path) 35 | : new ZipEntryFile(entry, path)); 36 | } 37 | } 38 | 39 | return [.. entries]; 40 | } 41 | 42 | protected override IEnumerable GetEntriesFromStream(Stream stream) 43 | { 44 | List entries = []; 45 | using (ZipFile zip = new(stream, leaveOpen: true)) 46 | { 47 | foreach (ZipEntry entry in zip) 48 | { 49 | if (ShouldSkipEntry(entry.IsDirectory)) 50 | { 51 | continue; 52 | } 53 | 54 | if (!ShouldInclude(entry.Name) || ShouldExclude(entry.Name)) 55 | { 56 | continue; 57 | } 58 | 59 | entries.Add(entry.IsDirectory 60 | ? new ZipEntryDirectory(entry, stream) 61 | : new ZipEntryFile(entry, stream)); 62 | } 63 | } 64 | 65 | return [.. entries]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/ExpandEntryCommandBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using PSCompression.Extensions; 5 | using PSCompression.Exceptions; 6 | using System.ComponentModel; 7 | 8 | namespace PSCompression.Abstractions; 9 | 10 | [EditorBrowsable(EditorBrowsableState.Never)] 11 | public abstract class ExpandEntryCommandBase : PSCmdlet 12 | where T : EntryBase 13 | { 14 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 15 | public T[] InputObject { get; set; } = null!; 16 | 17 | [Parameter(Position = 0)] 18 | [ValidateNotNullOrEmpty] 19 | public string? Destination { get; set; } 20 | 21 | [Parameter] 22 | public SwitchParameter Force { get; set; } 23 | 24 | [Parameter] 25 | public SwitchParameter PassThru { get; set; } 26 | 27 | protected override void BeginProcessing() 28 | { 29 | Destination = Destination is null 30 | // PowerShell is retarded and decided to mix up ProviderPath & Path 31 | ? SessionState.Path.CurrentFileSystemLocation.ProviderPath 32 | : Destination.ResolvePath(this); 33 | 34 | if (File.Exists(Destination)) 35 | { 36 | ThrowTerminatingError(ExceptionHelper.NotDirectoryPath( 37 | Destination, nameof(Destination))); 38 | } 39 | 40 | Directory.CreateDirectory(Destination); 41 | } 42 | 43 | protected override void ProcessRecord() 44 | { 45 | Dbg.Assert(Destination is not null); 46 | 47 | foreach (T entry in InputObject) 48 | { 49 | try 50 | { 51 | FileSystemInfo info = Extract(entry); 52 | 53 | if (PassThru) 54 | { 55 | WriteObject(info.AppendPSProperties()); 56 | } 57 | } 58 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 59 | { 60 | throw; 61 | } 62 | catch (Exception exception) 63 | { 64 | WriteError(exception.ToExtractEntryError(entry)); 65 | } 66 | } 67 | } 68 | 69 | protected abstract FileSystemInfo Extract(T entry); 70 | } 71 | -------------------------------------------------------------------------------- /src/PSCompression.Shared/LoadContext.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Reflection; 3 | using System.Runtime.Loader; 4 | using System.IO; 5 | 6 | namespace PSCompression.Shared; 7 | 8 | [ExcludeFromCodeCoverage] 9 | public sealed class LoadContext : AssemblyLoadContext 10 | { 11 | private static LoadContext? _instance; 12 | 13 | private readonly static object s_sync = new(); 14 | 15 | private readonly Assembly _thisAssembly; 16 | 17 | private readonly AssemblyName _thisAssemblyName; 18 | 19 | private readonly Assembly _moduleAssembly; 20 | 21 | private readonly string _assemblyDir; 22 | 23 | private LoadContext(string mainModulePathAssemblyPath) 24 | : base(name: "PSCompression", isCollectible: false) 25 | { 26 | _assemblyDir = Path.GetDirectoryName(mainModulePathAssemblyPath) ?? ""; 27 | _thisAssembly = typeof(LoadContext).Assembly; 28 | _thisAssemblyName = _thisAssembly.GetName(); 29 | _moduleAssembly = LoadFromAssemblyPath(mainModulePathAssemblyPath); 30 | } 31 | 32 | protected override Assembly? Load(AssemblyName assemblyName) 33 | { 34 | if (AssemblyName.ReferenceMatchesDefinition(_thisAssemblyName, assemblyName)) 35 | { 36 | return _thisAssembly; 37 | } 38 | 39 | string asmPath = Path.Join(_assemblyDir, $"{assemblyName.Name}.dll"); 40 | if (File.Exists(asmPath)) 41 | { 42 | return LoadFromAssemblyPath(asmPath); 43 | } 44 | 45 | return null; 46 | } 47 | 48 | public static Assembly Initialize() 49 | { 50 | if (_instance is not null) 51 | { 52 | return _instance._moduleAssembly; 53 | } 54 | 55 | lock (s_sync) 56 | { 57 | string assemblyPath = typeof(LoadContext).Assembly.Location; 58 | string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); 59 | string moduleName = assemblyName[..^7]; 60 | string modulePath = Path.Combine( 61 | Path.GetDirectoryName(assemblyPath)!, 62 | $"{moduleName}.dll"); 63 | 64 | _instance = new LoadContext(modulePath); 65 | return _instance._moduleAssembly; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/ZipEntryBase.IO.Compression.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using PSCompression.Exceptions; 4 | 5 | namespace PSCompression.Abstractions; 6 | 7 | public abstract partial class ZipEntryBase 8 | { 9 | public ZipArchive OpenRead() => 10 | FromStream ? new ZipArchive(_stream) : ZipFile.OpenRead(Source); 11 | 12 | public ZipArchive OpenWrite() 13 | { 14 | this.ThrowIfFromStream(); 15 | return ZipFile.Open(Source, ZipArchiveMode.Update); 16 | } 17 | 18 | public void Remove() 19 | { 20 | this.ThrowIfFromStream(); 21 | 22 | using ZipArchive zip = ZipFile.Open( 23 | Source, 24 | ZipArchiveMode.Update); 25 | 26 | zip.ThrowIfNotFound( 27 | path: RelativePath, 28 | source: Source, 29 | out ZipArchiveEntry entry); 30 | 31 | entry.Delete(); 32 | } 33 | 34 | internal void Remove(ZipArchive zip) 35 | { 36 | this.ThrowIfFromStream(); 37 | 38 | zip.ThrowIfNotFound( 39 | path: RelativePath, 40 | source: Source, 41 | out ZipArchiveEntry entry); 42 | 43 | entry.Delete(); 44 | } 45 | 46 | internal static string Move( 47 | string sourceRelativePath, 48 | string destination, 49 | string sourceZipPath, 50 | ZipArchive zip) 51 | { 52 | zip.ThrowIfNotFound( 53 | path: sourceRelativePath, 54 | source: sourceZipPath, 55 | entry: out ZipArchiveEntry sourceEntry); 56 | 57 | zip.ThrowIfDuplicate( 58 | path: destination, 59 | source: sourceZipPath); 60 | 61 | destination.ThrowIfInvalidPathChar(); 62 | 63 | ZipArchiveEntry destinationEntry = zip.CreateEntry(destination); 64 | using (Stream sourceStream = sourceEntry.Open()) 65 | using (Stream destinationStream = destinationEntry.Open()) 66 | { 67 | sourceStream.CopyTo(destinationStream); 68 | } 69 | 70 | sourceEntry.Delete(); 71 | return destination; 72 | } 73 | 74 | internal ZipArchive OpenZip(ZipArchiveMode mode) => 75 | FromStream 76 | ? new ZipArchive(_stream, mode, true) 77 | : ZipFile.Open(Source, mode); 78 | } 79 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/GetZipEntryContentCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using System.Security; 5 | using ICSharpCode.SharpZipLib.Zip; 6 | using PSCompression.Abstractions; 7 | using PSCompression.Exceptions; 8 | using PSCompression.Extensions; 9 | 10 | namespace PSCompression.Commands; 11 | 12 | [Cmdlet(VerbsCommon.Get, "ZipEntryContent", DefaultParameterSetName = "Stream")] 13 | [OutputType(typeof(string), ParameterSetName = ["Stream"])] 14 | [OutputType(typeof(byte), ParameterSetName = ["Bytes"])] 15 | [Alias("zipgec")] 16 | public sealed class GetZipEntryContentCommand : GetEntryContentCommandBase, IDisposable 17 | { 18 | [Parameter] 19 | public SecureString? Password { get; set; } 20 | 21 | private ZipArchiveCache? _cache; 22 | 23 | protected override void ProcessRecord() 24 | { 25 | _cache ??= new ZipArchiveCache(entry => entry.OpenRead(Password)); 26 | 27 | foreach (ZipEntryFile entry in Entry) 28 | { 29 | try 30 | { 31 | ZipFile zip = _cache.GetOrCreate(entry); 32 | if (entry.IsEncrypted && Password is null) 33 | { 34 | zip.Password = entry.PromptForPassword(Host); 35 | } 36 | 37 | ReadEntry(entry.Open(zip)); 38 | } 39 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 40 | { 41 | throw; 42 | } 43 | catch (Exception exception) 44 | { 45 | WriteError(exception.ToOpenError(entry.Source)); 46 | } 47 | } 48 | } 49 | 50 | private void ReadEntry(Stream stream) 51 | { 52 | if (AsByteStream) 53 | { 54 | using EntryByteReader byteReader = new(stream, Buffer!); 55 | if (Raw) 56 | { 57 | byteReader.ReadAllBytes(this); 58 | return; 59 | } 60 | 61 | byteReader.StreamBytes(this); 62 | return; 63 | } 64 | 65 | using StreamReader stringReader = new(stream, Encoding); 66 | if (Raw) 67 | { 68 | stringReader.ReadToEnd(this); 69 | return; 70 | } 71 | 72 | stringReader.ReadLines(this); 73 | } 74 | 75 | public void Dispose() 76 | { 77 | _cache?.Dispose(); 78 | GC.SuppressFinalize(this); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/GetTarEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using System.Text; 5 | using ICSharpCode.SharpZipLib.Tar; 6 | using PSCompression.Abstractions; 7 | using PSCompression.Extensions; 8 | 9 | namespace PSCompression.Commands; 10 | 11 | [Cmdlet(VerbsCommon.Get, "TarEntry", DefaultParameterSetName = "Path")] 12 | [OutputType(typeof(TarEntryDirectory), typeof(TarEntryFile))] 13 | [Alias("targe")] 14 | public sealed class GetTarEntryCommand : GetEntryCommandBase 15 | { 16 | [Parameter] 17 | public Algorithm Algorithm { get; set; } 18 | 19 | internal override ArchiveType ArchiveType => ArchiveType.tar; 20 | 21 | protected override IEnumerable GetEntriesFromFile(string path) 22 | { 23 | if (!MyInvocation.BoundParameters.ContainsKey(nameof(Algorithm))) 24 | { 25 | Algorithm = AlgorithmMappings.Parse(path); 26 | } 27 | 28 | using FileStream fs = File.OpenRead(path); 29 | using Stream stream = Algorithm.FromCompressedStream(fs); 30 | using TarInputStream tar = new(stream, Encoding.UTF8); 31 | 32 | foreach (TarEntry entry in tar.EnumerateEntries()) 33 | { 34 | if (ShouldSkipEntry(entry.IsDirectory)) 35 | { 36 | continue; 37 | } 38 | 39 | if (!ShouldInclude(entry.Name) || ShouldExclude(entry.Name)) 40 | { 41 | continue; 42 | } 43 | 44 | yield return entry.IsDirectory 45 | ? new TarEntryDirectory(entry, path) 46 | : new TarEntryFile(entry, path, Algorithm); 47 | } 48 | } 49 | 50 | protected override IEnumerable GetEntriesFromStream(Stream stream) 51 | { 52 | Stream decompressStream = Algorithm.FromCompressedStream(stream); 53 | TarInputStream tar = new(decompressStream, Encoding.UTF8); 54 | 55 | foreach (TarEntry entry in tar.EnumerateEntries()) 56 | { 57 | if (ShouldSkipEntry(entry.IsDirectory)) 58 | { 59 | continue; 60 | } 61 | 62 | if (!ShouldInclude(entry.Name) || ShouldExclude(entry.Name)) 63 | { 64 | continue; 65 | } 66 | 67 | yield return entry.IsDirectory 68 | ? new TarEntryDirectory(entry, stream) 69 | : new TarEntryFile(entry, stream, Algorithm); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/PSCompression/TarEntryFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Text; 4 | using ICSharpCode.SharpZipLib.Tar; 5 | using PSCompression.Abstractions; 6 | using PSCompression.Extensions; 7 | 8 | namespace PSCompression; 9 | 10 | public sealed class TarEntryFile : TarEntryBase 11 | { 12 | private readonly Algorithm _algorithm; 13 | 14 | public string BaseName { get; } 15 | 16 | public string Extension { get; } 17 | 18 | public override EntryType Type => EntryType.Archive; 19 | 20 | internal TarEntryFile(TarEntry entry, string source, Algorithm algorithm) 21 | : base(entry, source) 22 | { 23 | Name = Path.GetFileName(entry.Name); 24 | BaseName = Path.GetFileNameWithoutExtension(Name); 25 | Extension = Path.GetExtension(RelativePath); 26 | _algorithm = algorithm; 27 | } 28 | 29 | internal TarEntryFile(TarEntry entry, Stream? stream, Algorithm algorithm) 30 | : base(entry, stream) 31 | { 32 | Name = Path.GetFileName(entry.Name); 33 | BaseName = Path.GetFileNameWithoutExtension(Name); 34 | Extension = Path.GetExtension(RelativePath); 35 | _algorithm = algorithm; 36 | } 37 | 38 | protected override string GetFormatDirectoryPath() => 39 | $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"; 40 | 41 | internal bool GetContentStream(Stream destination) 42 | { 43 | Stream? sourceStream = null; 44 | Stream? decompressedStream = null; 45 | TarInputStream? tar = null; 46 | 47 | try 48 | { 49 | sourceStream = _stream ?? File.OpenRead(Source); 50 | sourceStream.Seek(0, SeekOrigin.Begin); 51 | decompressedStream = _algorithm.FromCompressedStream(sourceStream); 52 | tar = new(decompressedStream, Encoding.UTF8); 53 | 54 | TarEntry? entry = tar 55 | .EnumerateEntries() 56 | .FirstOrDefault(e => e.Name == RelativePath); 57 | 58 | if (entry is null or { Size: 0 }) 59 | { 60 | return false; 61 | } 62 | 63 | tar.CopyTo(destination, (int)entry.Size); 64 | destination.Seek(0, SeekOrigin.Begin); 65 | return true; 66 | } 67 | finally 68 | { 69 | if (!FromStream) 70 | { 71 | tar?.Dispose(); 72 | decompressedStream?.Dispose(); 73 | sourceStream?.Dispose(); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/CommandWithPathBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | using System.Management.Automation; 6 | using PSCompression.Exceptions; 7 | using PSCompression.Extensions; 8 | 9 | namespace PSCompression.Abstractions; 10 | 11 | [EditorBrowsable(EditorBrowsableState.Never)] 12 | public abstract class CommandWithPathBase : PSCmdlet 13 | { 14 | protected string[] _paths = []; 15 | 16 | protected bool IsLiteral 17 | { 18 | get => MyInvocation.BoundParameters.ContainsKey("LiteralPath"); 19 | } 20 | 21 | [Parameter( 22 | ParameterSetName = "Path", 23 | Position = 0, 24 | Mandatory = true, 25 | ValueFromPipeline = true)] 26 | [SupportsWildcards] 27 | public virtual string[] Path 28 | { 29 | get => _paths; 30 | set => _paths = value; 31 | } 32 | 33 | [Parameter( 34 | ParameterSetName = "LiteralPath", 35 | Mandatory = true, 36 | ValueFromPipelineByPropertyName = true)] 37 | [Alias("PSPath")] 38 | public virtual string[] LiteralPath 39 | { 40 | get => _paths; 41 | set => _paths = value; 42 | } 43 | 44 | protected IEnumerable EnumerateResolvedPaths() 45 | { 46 | Collection resolvedPaths; 47 | ProviderInfo provider; 48 | 49 | foreach (string path in _paths) 50 | { 51 | if (IsLiteral) 52 | { 53 | string resolved = SessionState.Path.GetUnresolvedProviderPathFromPSPath( 54 | path: path, 55 | provider: out provider, 56 | drive: out _); 57 | 58 | if (provider.Validate(resolved, throwOnInvalidProvider: false, this)) 59 | { 60 | yield return resolved; 61 | } 62 | 63 | continue; 64 | } 65 | 66 | try 67 | { 68 | resolvedPaths = GetResolvedProviderPathFromPSPath(path, out provider); 69 | } 70 | catch (Exception exception) 71 | { 72 | WriteError(exception.ToResolvePathError(path)); 73 | continue; 74 | } 75 | 76 | 77 | foreach (string resolvedPath in resolvedPaths) 78 | { 79 | if (provider.Validate(resolvedPath, throwOnInvalidProvider: true, this)) 80 | { 81 | yield return resolvedPath; 82 | } 83 | } 84 | } 85 | } 86 | 87 | protected string ResolvePath(string path) => path.ResolvePath(this); 88 | } 89 | -------------------------------------------------------------------------------- /tests/EncodingTransformation.tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | using namespace System.Text 3 | 4 | $ErrorActionPreference = 'Stop' 5 | 6 | $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 7 | $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 8 | 9 | Import-Module $manifestPath 10 | Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) 11 | 12 | Describe 'EncodingTransformation Class' { 13 | BeforeAll { 14 | Add-Type -TypeDefinition ' 15 | public static class Acp 16 | { 17 | [System.Runtime.InteropServices.DllImport("Kernel32.dll")] 18 | public static extern int GetACP(); 19 | }' 20 | 21 | $encodings = @{ 22 | 'ascii' = [ASCIIEncoding]::new() 23 | 'bigendianunicode' = [UnicodeEncoding]::new($true, $true) 24 | 'bigendianutf32' = [UTF32Encoding]::new($true, $true) 25 | 'oem' = [Console]::OutputEncoding 26 | 'unicode' = [UnicodeEncoding]::new() 27 | 'utf8' = [UTF8Encoding]::new($false) 28 | 'utf8bom' = [UTF8Encoding]::new($true) 29 | 'utf8nobom' = [UTF8Encoding]::new($false) 30 | 'utf32' = [UTF32Encoding]::new() 31 | } 32 | 33 | if ($osIsWindows) { 34 | $encodings['ansi'] = [Encoding]::GetEncoding([Acp]::GetACP()) 35 | } 36 | 37 | $transform = [PSCompression.EncodingTransformation]::new() 38 | $transform | Out-Null 39 | } 40 | 41 | It 'Transforms Encoding to Encoding' { 42 | $transform.Transform($ExecutionContext, [Encoding]::UTF8) | 43 | Should -BeExactly ([Encoding]::UTF8) 44 | } 45 | 46 | It 'Transforms a completion set to their Encoding Representations' { 47 | $encodings.GetEnumerator() | ForEach-Object { 48 | $transform.Transform($ExecutionContext, $_.Key) | 49 | Should -BeExactly $_.Value 50 | } 51 | } 52 | 53 | It 'Transforms CodePage to their Encoding Representations' { 54 | [System.Text.Encoding]::GetEncodings() | ForEach-Object { 55 | $transform.Transform($ExecutionContext, $_.CodePage) | 56 | Should -BeExactly $_.GetEncoding() 57 | } 58 | } 59 | 60 | It 'Throws if input value cannot be transformed' { 61 | { $transform.Transform($ExecutionContext, 'doesnotexist') } | 62 | Should -Throw 63 | } 64 | 65 | It 'Throws if the input value type is not Encoding, string or int' { 66 | { $transform.Transform($ExecutionContext, [type]) } | 67 | Should -Throw 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/CompressZipArchiveCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Management.Automation; 5 | using PSCompression.Extensions; 6 | using PSCompression.Exceptions; 7 | using PSCompression.Abstractions; 8 | 9 | namespace PSCompression.Commands; 10 | 11 | [Cmdlet(VerbsData.Compress, "ZipArchive")] 12 | [OutputType(typeof(FileInfo))] 13 | [Alias("zipcompress")] 14 | public sealed class CompressZipArchiveCommand : ToCompressedFileCommandBase 15 | { 16 | private ZipArchiveMode ZipArchiveMode 17 | { 18 | get => Force.IsPresent || Update.IsPresent 19 | ? ZipArchiveMode.Update 20 | : ZipArchiveMode.Create; 21 | } 22 | 23 | protected override string FileExtension => ".zip"; 24 | 25 | private void CreateEntry( 26 | FileInfo file, 27 | ZipArchive zip, 28 | string relativepath) 29 | { 30 | try 31 | { 32 | using FileStream fileStream = OpenFileStream(file); 33 | using Stream stream = zip 34 | .CreateEntry(relativepath, CompressionLevel) 35 | .Open(); 36 | 37 | fileStream.CopyTo(stream); 38 | } 39 | catch (Exception exception) 40 | { 41 | WriteError(exception.ToStreamOpenError(file.FullName)); 42 | } 43 | } 44 | 45 | private void UpdateEntry( 46 | FileInfo file, 47 | ZipArchiveEntry entry) 48 | { 49 | try 50 | { 51 | using FileStream fileStream = OpenFileStream(file); 52 | using Stream stream = entry.Open(); 53 | stream.SetLength(0); 54 | fileStream.CopyTo(stream); 55 | } 56 | catch (Exception exception) 57 | { 58 | WriteError(exception.ToStreamOpenError(file.FullName)); 59 | } 60 | } 61 | 62 | protected override ZipArchive CreateCompressionStream(Stream outputStream) => 63 | new(outputStream, ZipArchiveMode); 64 | 65 | protected override void CreateDirectoryEntry( 66 | ZipArchive archive, 67 | DirectoryInfo directory, 68 | string path) 69 | { 70 | if (!Update || !archive.TryGetEntry(path, out _)) 71 | { 72 | archive.CreateEntry(path); 73 | } 74 | } 75 | 76 | protected override void CreateOrUpdateFileEntry( 77 | ZipArchive archive, 78 | FileInfo file, 79 | string path) 80 | { 81 | if (Update && archive.TryGetEntry(path, out ZipArchiveEntry? entry)) 82 | { 83 | UpdateEntry(file, entry); 84 | return; 85 | } 86 | 87 | CreateEntry(file, archive, path); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Project.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace ProjectBuilder; 8 | 9 | public sealed class Project 10 | { 11 | public DirectoryInfo Source { get; } 12 | 13 | public string Build { get; } 14 | 15 | public string? Release { get; internal set; } 16 | 17 | public string[]? TargetFrameworks { get; internal set; } 18 | 19 | public string? TestFramework 20 | { 21 | get 22 | { 23 | if (TargetFrameworks is { Length: 1 }) 24 | { 25 | return TargetFrameworks.First(); 26 | } 27 | 28 | return _info.PowerShellVersion is { Major: 5, Minor: 1 } 29 | ? TargetFrameworks 30 | .Where(e => Regex.Match(e, "^net(?:4|standard)").Success) 31 | .FirstOrDefault() 32 | 33 | : TargetFrameworks 34 | .Where(e => !Regex.Match(e, "^net(?:4|standard)").Success) 35 | .FirstOrDefault(); 36 | } 37 | } 38 | 39 | private Configuration Configuration { get => _info.Configuration; } 40 | 41 | private readonly ProjectInfo _info; 42 | 43 | internal Project(DirectoryInfo source, string build, ProjectInfo info) 44 | { 45 | Source = source; 46 | Build = build; 47 | _info = info; 48 | } 49 | 50 | public void CopyToRelease() 51 | { 52 | if (TargetFrameworks is null) 53 | { 54 | throw new ArgumentNullException( 55 | "TargetFrameworks is null.", 56 | nameof(TargetFrameworks)); 57 | } 58 | 59 | foreach (string framework in TargetFrameworks) 60 | { 61 | DirectoryInfo buildFolder = new(Path.Combine( 62 | Source.FullName, 63 | "bin", 64 | Configuration.ToString(), 65 | framework, 66 | "publish")); 67 | 68 | string binFolder = Path.Combine(Release, "bin", framework); 69 | buildFolder.CopyRecursive(binFolder); 70 | } 71 | } 72 | 73 | public Hashtable GetPSRepoParams() => new() 74 | { 75 | ["Name"] = "LocalRepo", 76 | ["SourceLocation"] = Build, 77 | ["PublishLocation"] = Build, 78 | ["InstallationPolicy"] = "Trusted" 79 | }; 80 | 81 | public void ClearNugetPackage() 82 | { 83 | string nugetPath = Path.Combine( 84 | Build, 85 | $"{_info.Module.Name}.{_info.Module.Version}.nupkg"); 86 | 87 | if (File.Exists(nugetPath)) 88 | { 89 | File.Delete(nugetPath); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using ICSharpCode.SharpZipLib.Zip; 4 | using PSCompression.Abstractions; 5 | using PSCompression.Exceptions; 6 | using PSCompression.Extensions; 7 | 8 | namespace PSCompression; 9 | 10 | public sealed class ZipEntryFile : ZipEntryBase 11 | { 12 | public string CompressionRatio { get; } 13 | 14 | public override EntryType Type { get => EntryType.Archive; } 15 | 16 | public string BaseName { get; } 17 | 18 | public string Extension { get; } 19 | 20 | internal ZipEntryFile(ZipEntry entry, string source) 21 | : base(entry, source) 22 | { 23 | CompressionRatio = GetRatio(Length, CompressedLength); 24 | Name = Path.GetFileName(entry.Name); 25 | BaseName = Path.GetFileNameWithoutExtension(Name); 26 | Extension = Path.GetExtension(RelativePath); 27 | } 28 | 29 | internal ZipEntryFile(ZipEntry entry, Stream? stream) 30 | : base(entry, stream) 31 | { 32 | CompressionRatio = GetRatio(Length, CompressedLength); 33 | Name = Path.GetFileName(entry.Name); 34 | BaseName = Path.GetFileNameWithoutExtension(Name); 35 | Extension = Path.GetExtension(RelativePath); 36 | } 37 | 38 | private static string GetRatio(long size, long compressedSize) 39 | { 40 | float compressedRatio = (float)compressedSize / size; 41 | 42 | if (float.IsNaN(compressedRatio)) 43 | { 44 | compressedRatio = 0; 45 | } 46 | 47 | return string.Format("{0:F2}%", 100 - (compressedRatio * 100)); 48 | } 49 | 50 | internal Stream Open(ZipArchive zip) 51 | { 52 | zip.ThrowIfNotFound( 53 | path: RelativePath, 54 | source: Source, 55 | out ZipArchiveEntry? entry); 56 | 57 | return entry.Open(); 58 | } 59 | 60 | internal Stream Open(ICSharpCode.SharpZipLib.Zip.ZipFile zip) 61 | { 62 | zip.ThrowIfNotFound( 63 | path: RelativePath, 64 | source: Source, 65 | out ZipEntry? entry); 66 | 67 | return zip.GetInputStream(entry); 68 | } 69 | 70 | internal void Refresh() 71 | { 72 | this.ThrowIfFromStream(); 73 | using ZipArchive zip = OpenRead(); 74 | Refresh(zip); 75 | } 76 | 77 | internal void Refresh(ZipArchive zip) 78 | { 79 | if (zip.TryGetEntry(RelativePath, out ZipArchiveEntry? entry)) 80 | { 81 | Length = entry.Length; 82 | CompressedLength = entry.CompressedLength; 83 | } 84 | } 85 | 86 | protected override string GetFormatDirectoryPath() => 87 | $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"; 88 | } 89 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/ToCompressedStringCommandBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Management.Automation; 6 | using System.Text; 7 | using PSCompression.Exceptions; 8 | using PSCompression.Extensions; 9 | 10 | namespace PSCompression.Abstractions; 11 | 12 | [EditorBrowsable(EditorBrowsableState.Never)] 13 | public abstract class ToCompressedStringCommandBase : PSCmdlet, IDisposable 14 | { 15 | private StreamWriter? _writer; 16 | 17 | private Stream? _compressStream; 18 | 19 | private readonly MemoryStream _outstream = new(); 20 | 21 | [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] 22 | [AllowEmptyString] 23 | public string[] InputObject { get; set; } = null!; 24 | 25 | [Parameter] 26 | [ArgumentCompleter(typeof(EncodingCompleter))] 27 | [EncodingTransformation] 28 | [ValidateNotNullOrEmpty] 29 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 30 | 31 | [Parameter] 32 | public CompressionLevel CompressionLevel { get; set; } = CompressionLevel.Optimal; 33 | 34 | [Parameter] 35 | [Alias("Raw")] 36 | public SwitchParameter AsByteStream { get; set; } 37 | 38 | [Parameter] 39 | public SwitchParameter NoNewLine { get; set; } 40 | 41 | protected abstract Stream CreateCompressionStream( 42 | Stream outputStream, 43 | CompressionLevel compressionLevel); 44 | 45 | protected override void ProcessRecord() 46 | { 47 | try 48 | { 49 | _compressStream ??= CreateCompressionStream(_outstream, CompressionLevel); 50 | _writer ??= new StreamWriter(_compressStream, Encoding); 51 | 52 | if (NoNewLine.IsPresent) 53 | { 54 | _writer.WriteContent(InputObject); 55 | return; 56 | } 57 | 58 | _writer.WriteLines(InputObject); 59 | } 60 | catch (Exception exception) 61 | { 62 | WriteError(exception.ToWriteError(InputObject)); 63 | } 64 | } 65 | 66 | protected override void EndProcessing() 67 | { 68 | _writer?.Dispose(); 69 | _compressStream?.Dispose(); 70 | _outstream.Dispose(); 71 | 72 | if (AsByteStream) 73 | { 74 | // On purpose, we don't want to enumerate the byte[] for efficiency 75 | WriteObject(_outstream.ToArray(), enumerateCollection: false); 76 | return; 77 | } 78 | 79 | WriteObject(Convert.ToBase64String(_outstream.ToArray())); 80 | } 81 | 82 | public void Dispose() 83 | { 84 | _writer?.Dispose(); 85 | _compressStream?.Dispose(); 86 | _outstream.Dispose(); 87 | GC.SuppressFinalize(this); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/FileEntryTypes.tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | using namespace System.IO.Compression 3 | 4 | $ErrorActionPreference = 'Stop' 5 | 6 | $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 7 | $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 8 | 9 | Import-Module $manifestPath 10 | Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) 11 | 12 | Describe 'File Entry Types' { 13 | BeforeAll { 14 | $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 15 | 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt 16 | 17 | $tarArchive = New-Item (Join-Path $TestDrive helloworld.txt) -ItemType File -Force | 18 | Compress-TarArchive -Destination 'testTarDirectory' -PassThru 19 | 20 | $tarArchive | Out-Null 21 | } 22 | 23 | It 'Should be of type Archive' { 24 | ($zip | Get-ZipEntry).Type | Should -BeExactly ([PSCompression.EntryType]::Archive) 25 | 26 | ($tarArchive | Get-TarEntry).Type | Should -BeExactly ([PSCompression.EntryType]::Archive) 27 | } 28 | 29 | It 'Should Have a BaseName Property' { 30 | ($zip | Get-ZipEntry).BaseName | Should -BeOfType ([string]) 31 | ($zip | Get-ZipEntry).BaseName | Should -BeExactly helloworld 32 | 33 | ($tarArchive | Get-TarEntry).BaseName | Should -BeOfType ([string]) 34 | ($tarArchive | Get-TarEntry).BaseName | Should -BeExactly helloworld 35 | } 36 | 37 | It 'Should Have an Extension Property' { 38 | ($zip | Get-ZipEntry).Extension | Should -BeOfType ([string]) 39 | ($zip | Get-ZipEntry).Extension | Should -BeExactly .txt 40 | 41 | ($tarArchive | Get-TarEntry).Extension | Should -BeOfType ([string]) 42 | ($tarArchive | Get-TarEntry).Extension | Should -BeExactly .txt 43 | } 44 | 45 | It 'Should Have an IsEncrypted Property' { 46 | ($zip | Get-ZipEntry).IsEncrypted | Should -BeOfType ([bool]) 47 | ($zip | Get-ZipEntry).IsEncrypted | Should -BeFalse 48 | } 49 | 50 | It 'Should Have an AESKeySize Property' { 51 | ($zip | Get-ZipEntry).AESKeySize | Should -BeOfType ([int]) 52 | ($zip | Get-ZipEntry).AESKeySize | Should -BeExactly 0 53 | } 54 | 55 | It 'Should Have a CompressionMethod Property' { 56 | ($zip | Get-ZipEntry).CompressionMethod | Should -Be Deflated 57 | } 58 | 59 | It 'Should Have a Comment Property' { 60 | ($zip | Get-ZipEntry).Comment | Should -BeOfType ([string]) 61 | ($zip | Get-ZipEntry).Comment | Should -BeExactly '' 62 | } 63 | 64 | It 'Should Open the source zip' { 65 | Use-Object ($stream = ($zip | Get-ZipEntry).OpenRead()) { 66 | $stream | Should -BeOfType ([ZipArchive]) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/CompressTarArchiveCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | using System.Management.Automation; 5 | using System.Text; 6 | using ICSharpCode.SharpZipLib.Tar; 7 | using PSCompression.Abstractions; 8 | using PSCompression.Exceptions; 9 | using PSCompression.Extensions; 10 | 11 | namespace PSCompression.Commands; 12 | 13 | [Cmdlet(VerbsData.Compress, "TarArchive")] 14 | [OutputType(typeof(FileInfo))] 15 | [Alias("tarcompress")] 16 | public sealed class CompressTarArchiveCommand : ToCompressedFileCommandBase 17 | { 18 | private Stream? _compressionStream; 19 | 20 | // override this parameter without adding the decoration since this isn't supported for .tar 21 | [ExcludeFromCodeCoverage] 22 | public override SwitchParameter Update { get; set; } 23 | 24 | [Parameter] 25 | [ValidateNotNull] 26 | public Algorithm Algorithm { get; set; } = Algorithm.gz; 27 | 28 | protected override string FileExtension 29 | { 30 | get => Algorithm == Algorithm.none ? ".tar" : $".tar.{Algorithm}"; 31 | } 32 | 33 | protected override TarOutputStream CreateCompressionStream(Stream outputStream) 34 | { 35 | if (Algorithm == Algorithm.lz && 36 | MyInvocation.BoundParameters.ContainsKey(nameof(CompressionLevel))) 37 | { 38 | WriteWarning( 39 | "The lzip algorithm does not support custom CompressionLevel settings. " + 40 | "The default compression level will be used."); 41 | } 42 | 43 | _compressionStream = Algorithm.ToCompressedStream(outputStream, CompressionLevel); 44 | return new TarOutputStream(_compressionStream, Encoding.UTF8); 45 | } 46 | 47 | protected override void CreateDirectoryEntry( 48 | TarOutputStream archive, 49 | DirectoryInfo directory, 50 | string path) 51 | { 52 | archive.CreateTarEntry( 53 | entryName: path, 54 | modTime: directory.LastWriteTimeUtc, 55 | size: 0); 56 | 57 | archive.CloseEntry(); 58 | } 59 | 60 | protected override void CreateOrUpdateFileEntry( 61 | TarOutputStream archive, 62 | FileInfo file, 63 | string path) 64 | { 65 | archive.CreateTarEntry( 66 | entryName: path, 67 | modTime: file.LastWriteTimeUtc, 68 | size: file.Length); 69 | 70 | try 71 | { 72 | using FileStream fs = OpenFileStream(file); 73 | fs.CopyTo(archive); 74 | } 75 | catch (Exception exception) 76 | { 77 | WriteError(exception.ToStreamOpenError(file.FullName)); 78 | } 79 | finally 80 | { 81 | archive.CloseEntry(); 82 | } 83 | } 84 | 85 | protected override void Dispose(bool disposing) 86 | { 87 | base.Dispose(disposing); 88 | _compressionStream?.Dispose(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/StringCompressionExpansionCommands.tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | using namespace System.IO.Compression 3 | 4 | $ErrorActionPreference = 'Stop' 5 | 6 | $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 7 | $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 8 | 9 | Import-Module $manifestPath 10 | Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) 11 | 12 | Describe 'String Compression & Expansion Commands' { 13 | BeforeAll { 14 | $conversionCommands = @{ 15 | 'ConvertFrom-BrotliString' = 'ConvertTo-BrotliString' 16 | 'ConvertFrom-DeflateString' = 'ConvertTo-DeflateString' 17 | 'ConvertFrom-GzipString' = 'ConvertTo-GzipString' 18 | 'ConvertFrom-ZlibString' = 'ConvertTo-ZlibString' 19 | } 20 | 21 | $conversionCommands | Out-Null 22 | } 23 | 24 | Context 'ConvertFrom Commands' -Tag 'ConvertFrom Commands' { 25 | It 'Should throw on a non b64 encoded input' { 26 | $conversionCommands.Keys | ForEach-Object { 27 | { 'foo' | & $_ } | Should -Throw -ExceptionType ([FormatException]) 28 | } 29 | } 30 | } 31 | 32 | Context 'ConvertTo & ConvertFrom General Usage' -Tag 'ConvertTo & ConvertFrom General Usage' { 33 | BeforeAll { 34 | $content = 'hello', 'world', '!' 35 | $content | Out-Null 36 | } 37 | 38 | It 'Can compress strings and expand strings' { 39 | foreach ($level in [CompressionLevel].GetEnumNames()) { 40 | $conversionCommands.Keys | ForEach-Object { 41 | $encoded = $content | & $conversionCommands[$_] -CompressionLevel $level 42 | $encoded | & $_ | Should -BeExactly $content 43 | } 44 | } 45 | } 46 | 47 | It 'Can compress strings outputting raw bytes' { 48 | $conversionCommands.Keys | ForEach-Object { 49 | [byte[]] $bytes = $content | & $conversionCommands[$_] -AsByteStream 50 | $result = [Convert]::ToBase64String($bytes) | & $_ 51 | $result | Should -BeExactly $content 52 | } 53 | } 54 | 55 | It 'Can expand b64 compressed strings and output a multi-line string' { 56 | $contentAsString = $content -join [Environment]::NewLine 57 | $conversionCommands.Keys | ForEach-Object { 58 | $content | & $conversionCommands[$_] | & $_ -Raw | 59 | ForEach-Object TrimEnd | Should -BeExactly $contentAsString 60 | } 61 | } 62 | 63 | It 'Concatenates strings when the -NoNewLine switch is used' { 64 | $contentAsString = -join $content 65 | $conversionCommands.Keys | ForEach-Object { 66 | $content | & $conversionCommands[$_] -NoNewLine | & $_ | 67 | Should -BeExactly $contentAsString 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tools/InvokeBuild.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [Parameter(Mandatory)] 4 | [ProjectBuilder.ProjectInfo] $ProjectInfo 5 | ) 6 | 7 | task Clean { 8 | $ProjectInfo.CleanRelease() 9 | } 10 | 11 | task BuildDocs { 12 | $helpParams = $ProjectInfo.Documentation.GetParams() 13 | $null = New-ExternalHelp @helpParams 14 | } 15 | 16 | task BuildManaged { 17 | $arguments = $ProjectInfo.GetBuildArgs() 18 | Push-Location -LiteralPath $ProjectInfo.Project.Source.FullName 19 | 20 | try { 21 | foreach ($framework in $ProjectInfo.Project.TargetFrameworks) { 22 | if ($framework -match '^net4' -and -not $IsWindows) { 23 | continue 24 | } 25 | 26 | Write-Host "Compiling for $framework" 27 | dotnet @arguments --framework $framework 28 | 29 | if ($LASTEXITCODE) { 30 | throw "Failed to compiled code for $framework" 31 | } 32 | } 33 | } 34 | finally { 35 | Pop-Location 36 | } 37 | } 38 | 39 | task CopyToRelease { 40 | $ProjectInfo.Module.CopyToRelease() 41 | $ProjectInfo.Project.CopyToRelease() 42 | } 43 | 44 | task Package { 45 | $ProjectInfo.Project.ClearNugetPackage() 46 | $repoParams = $ProjectInfo.Project.GetPSRepoParams() 47 | 48 | if (Get-PSRepository -Name $repoParams.Name -ErrorAction SilentlyContinue) { 49 | Unregister-PSRepository -Name $repoParams.Name 50 | } 51 | 52 | Register-PSRepository @repoParams 53 | try { 54 | $publishModuleSplat = @{ 55 | Path = $ProjectInfo.Project.Release 56 | Repository = $repoParams.Name 57 | } 58 | Publish-Module @publishModuleSplat 59 | } 60 | finally { 61 | Unregister-PSRepository -Name $repoParams.Name 62 | } 63 | } 64 | 65 | task Analyze { 66 | if (-not $ProjectInfo.AnalyzerPath) { 67 | Write-Host 'No Analyzer Settings found, skipping' 68 | return 69 | } 70 | 71 | $pssaSplat = $ProjectInfo.GetAnalyzerParams() 72 | $results = Invoke-ScriptAnalyzer @pssaSplat 73 | 74 | if ($results) { 75 | $results | Out-String 76 | throw 'Failed PsScriptAnalyzer tests, build failed' 77 | } 78 | } 79 | 80 | task PesterTests { 81 | if (-not $ProjectInfo.Pester.PesterScript) { 82 | Write-Host 'No Pester tests found, skipping' 83 | return 84 | } 85 | 86 | $ProjectInfo.Pester.ClearResultFile() 87 | 88 | if (-not (dotnet tool list --global | Select-String coverlet.console -SimpleMatch)) { 89 | Write-Host 'Installing dotnet tool coverlet.console' -ForegroundColor Yellow 90 | dotnet tool install --global coverlet.console 91 | } 92 | 93 | coverlet $ProjectInfo.Pester.GetTestArgs($PSVersionTable.PSVersion) 94 | 95 | if ($LASTEXITCODE) { 96 | throw 'Pester failed tests' 97 | } 98 | } 99 | 100 | task Build -Jobs Clean, BuildManaged, CopyToRelease, BuildDocs, Package 101 | task Test -Jobs BuildManaged, Analyze, PesterTests 102 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/ZipEntryBase.SharpZipLib.Zip.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security; 4 | using ICSharpCode.SharpZipLib.Zip; 5 | using PSCompression.Exceptions; 6 | using PSCompression.Extensions; 7 | 8 | namespace PSCompression.Abstractions; 9 | 10 | public abstract partial class ZipEntryBase(ZipEntry entry, string source) 11 | : EntryBase(source) 12 | { 13 | public override string? Name { get; protected set; } 14 | 15 | public override string RelativePath { get; } = entry.Name; 16 | 17 | public override DateTime LastWriteTime { get; } = entry.DateTime; 18 | 19 | public override long Length { get; internal set; } = entry.Size; 20 | 21 | public long CompressedLength { get; internal set; } = entry.CompressedSize; 22 | 23 | public bool IsEncrypted { get; } = entry.IsCrypted; 24 | 25 | public int AESKeySize { get; } = entry.AESKeySize; 26 | 27 | public CompressionMethod CompressionMethod { get; } = entry.CompressionMethod; 28 | 29 | public string Comment { get; } = entry.Comment ?? string.Empty; 30 | 31 | public long Crc { get; } = entry.Crc; 32 | 33 | protected ZipEntryBase(ZipEntry entry, Stream? stream) 34 | : this(entry, $"InputStream.{Guid.NewGuid()}") 35 | { 36 | _stream = stream; 37 | } 38 | 39 | internal ZipFile OpenRead(SecureString? password) 40 | { 41 | ZipFile zip = FromStream ? new(_stream, leaveOpen: true) : new(Source); 42 | if (password?.Length > 0) 43 | { 44 | zip.Password = password.AsPlainText(); 45 | } 46 | 47 | return zip; 48 | } 49 | 50 | public FileSystemInfo ExtractTo( 51 | string destination, 52 | bool overwrite, 53 | SecureString? password = null) 54 | { 55 | using ZipFile zip = FromStream 56 | ? new(_stream, leaveOpen: true) 57 | : new(Source); 58 | 59 | if (password?.Length > 0) 60 | { 61 | zip.Password = password.AsPlainText(); 62 | } 63 | 64 | return ExtractTo(destination, overwrite, zip); 65 | } 66 | 67 | internal FileSystemInfo ExtractTo( 68 | string destination, 69 | bool overwrite, 70 | ZipFile zip) 71 | { 72 | zip.ThrowIfNotFound( 73 | RelativePath, 74 | Source, 75 | out ZipEntry? entry); 76 | 77 | destination = Path.GetFullPath( 78 | Path.Combine(destination, RelativePath)); 79 | 80 | if (Type == EntryType.Directory) 81 | { 82 | DirectoryInfo dir = new(destination); 83 | dir.Create(overwrite); 84 | return dir; 85 | } 86 | 87 | FileInfo file = new(destination); 88 | file.Directory!.Create(); 89 | 90 | using Stream source = zip.GetInputStream(entry); 91 | using FileStream fs = file.Open( 92 | overwrite ? FileMode.Create : FileMode.CreateNew, 93 | FileAccess.Write); 94 | 95 | source.CopyTo(fs); 96 | 97 | return file; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/PSCompression/OnModuleImportAndRemove.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.IO; 5 | using System.Management.Automation; 6 | using System.Reflection; 7 | using System.Runtime.InteropServices; 8 | 9 | /// 10 | /// OnModuleImportAndRemove is a class that implements the IModuleAssemblyInitializer and IModuleAssemblyCleanup interfaces. 11 | /// This class is used to handle the assembly resolve event when the module is imported and removed. 12 | /// 13 | [ExcludeFromCodeCoverage] 14 | public class OnModuleImportAndRemove : IModuleAssemblyInitializer, IModuleAssemblyCleanup 15 | { 16 | /// 17 | /// OnImport is called when the module is imported. 18 | /// 19 | public void OnImport() 20 | { 21 | if (IsNetFramework()) 22 | { 23 | AppDomain.CurrentDomain.AssemblyResolve += MyResolveEventHandler; 24 | } 25 | } 26 | 27 | /// 28 | /// OnRemove is called when the module is removed. 29 | /// 30 | /// 31 | public void OnRemove(PSModuleInfo module) 32 | { 33 | if (IsNetFramework()) 34 | { 35 | AppDomain.CurrentDomain.AssemblyResolve -= MyResolveEventHandler; 36 | } 37 | } 38 | 39 | /// 40 | /// MyResolveEventHandler is a method that handles the AssemblyResolve event. 41 | /// 42 | /// 43 | /// 44 | /// 45 | private static Assembly? MyResolveEventHandler(object? sender, ResolveEventArgs args) 46 | { 47 | string? libDirectory = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location); 48 | List directoriesToSearch = []; 49 | 50 | if (!string.IsNullOrEmpty(libDirectory)) 51 | { 52 | directoriesToSearch.Add(libDirectory); 53 | if (Directory.Exists(libDirectory)) 54 | { 55 | IEnumerable dirs = Directory.EnumerateDirectories( 56 | libDirectory, "*", SearchOption.AllDirectories); 57 | 58 | directoriesToSearch.AddRange(dirs); 59 | } 60 | } 61 | 62 | string requestedAssemblyName = new AssemblyName(args.Name).Name + ".dll"; 63 | 64 | foreach (string directory in directoriesToSearch) 65 | { 66 | string assemblyPath = Path.Combine(directory, requestedAssemblyName); 67 | 68 | if (File.Exists(assemblyPath)) 69 | { 70 | try 71 | { 72 | return Assembly.LoadFrom(assemblyPath); 73 | } 74 | catch (Exception ex) 75 | { 76 | Console.WriteLine($"Failed to load assembly from {assemblyPath}: {ex.Message}"); 77 | } 78 | } 79 | } 80 | 81 | return null; 82 | } 83 | 84 | /// 85 | /// Determine if the current runtime is .NET Framework 86 | /// 87 | /// 88 | private static bool IsNetFramework() => RuntimeInformation.FrameworkDescription 89 | .StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase); 90 | } 91 | -------------------------------------------------------------------------------- /src/PSCompression/ZLibStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | using System.IO.Compression; 5 | 6 | namespace PSCompression; 7 | 8 | // ain't nobody got time for that 9 | [ExcludeFromCodeCoverage] 10 | internal sealed class ZlibStream : Stream 11 | { 12 | private readonly Stream _outputStream; 13 | 14 | private readonly DeflateStream _deflateStream; 15 | 16 | private readonly MemoryStream _uncompressedBuffer; 17 | 18 | private bool _isDisposed; 19 | 20 | internal ZlibStream(Stream outputStream, CompressionLevel compressionLevel) 21 | { 22 | _outputStream = outputStream ?? throw new ArgumentNullException(nameof(outputStream)); 23 | _uncompressedBuffer = new MemoryStream(); 24 | 25 | // Write zlib header (0x78 0x9C for default compatibility) 26 | _outputStream.WriteByte(0x78); 27 | _outputStream.WriteByte(0x9C); 28 | _deflateStream = new DeflateStream(outputStream, compressionLevel, true); 29 | } 30 | 31 | public override bool CanRead => false; 32 | 33 | public override bool CanSeek => false; 34 | 35 | public override bool CanWrite => true; 36 | 37 | public override long Length => throw new NotSupportedException(); 38 | 39 | public override long Position 40 | { 41 | get => throw new NotSupportedException(); 42 | set => throw new NotSupportedException(); 43 | } 44 | 45 | public override void Flush() 46 | { 47 | _deflateStream.Flush(); 48 | _outputStream.Flush(); 49 | } 50 | 51 | public override int Read(byte[] buffer, int offset, int count) => 52 | throw new NotSupportedException("Reading is not supported."); 53 | 54 | public override long Seek(long offset, SeekOrigin origin) => 55 | throw new NotSupportedException("Seeking is not supported."); 56 | 57 | public override void SetLength(long value) => 58 | throw new NotSupportedException("Setting length is not supported."); 59 | 60 | public override void Write(byte[] buffer, int offset, int count) 61 | { 62 | _uncompressedBuffer.Write(buffer, offset, count); 63 | _deflateStream.Write(buffer, offset, count); 64 | } 65 | 66 | private uint ComputeAdler32() 67 | { 68 | const uint MOD_ADLER = 65521; 69 | uint a = 1, b = 0; 70 | byte[] data = _uncompressedBuffer.ToArray(); 71 | 72 | foreach (byte byt in data) 73 | { 74 | a = (a + byt) % MOD_ADLER; 75 | b = (b + a) % MOD_ADLER; 76 | } 77 | 78 | return (b << 16) | a; 79 | } 80 | 81 | protected override void Dispose(bool disposing) 82 | { 83 | if (_isDisposed) 84 | { 85 | return; 86 | } 87 | 88 | if (disposing) 89 | { 90 | _deflateStream.Dispose(); 91 | 92 | uint adler32 = ComputeAdler32(); 93 | byte[] checksum = BitConverter.GetBytes(adler32); 94 | if (BitConverter.IsLittleEndian) 95 | { 96 | Array.Reverse(checksum); 97 | } 98 | 99 | _outputStream.Write(checksum, 0, checksum.Length); 100 | _uncompressedBuffer.Dispose(); 101 | } 102 | 103 | _isDisposed = true; 104 | base.Dispose(disposing); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryMoveCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using PSCompression.Abstractions; 7 | using PSCompression.Extensions; 8 | 9 | namespace PSCompression; 10 | 11 | internal sealed class ZipEntryMoveCache 12 | { 13 | private readonly Dictionary> _cache; 14 | 15 | private readonly Dictionary> _mappings; 16 | 17 | internal ZipEntryMoveCache() 18 | { 19 | _cache = new(StringComparer.InvariantCultureIgnoreCase); 20 | _mappings = []; 21 | } 22 | 23 | private Dictionary WithSource(ZipEntryBase entry) 24 | { 25 | if (!_cache.ContainsKey(entry.Source)) 26 | { 27 | _cache[entry.Source] = []; 28 | } 29 | 30 | return _cache[entry.Source]; 31 | } 32 | 33 | internal bool IsDirectoryEntry(string source, string path) => 34 | _cache[source].TryGetValue(path, out EntryWithPath entryWithPath) 35 | && entryWithPath.ZipEntry.Type is EntryType.Directory; 36 | 37 | internal void AddEntry(ZipEntryBase entry, string newname) => 38 | WithSource(entry).Add(entry.RelativePath, new(entry, newname)); 39 | 40 | internal IEnumerable<(string, PathWithType)> GetPassThruMappings() 41 | { 42 | foreach (var source in _cache) 43 | { 44 | foreach ((string path, EntryWithPath entryWithPath) in source.Value) 45 | { 46 | yield return ( 47 | source.Key, 48 | new PathWithType( 49 | _mappings[source.Key][path], 50 | entryWithPath.ZipEntry.Type)); 51 | } 52 | } 53 | } 54 | 55 | internal Dictionary> GetMappings( 56 | ZipArchiveCache cache) 57 | { 58 | foreach (var source in _cache) 59 | { 60 | _mappings[source.Key] = GetChildMappings(cache, source.Value); 61 | } 62 | 63 | return _mappings; 64 | } 65 | 66 | private Dictionary GetChildMappings( 67 | ZipArchiveCache cache, 68 | Dictionary pathChanges) 69 | { 70 | string newpath; 71 | Dictionary result = []; 72 | 73 | foreach (var pair in pathChanges.OrderByDescending(e => e.Key)) 74 | { 75 | (ZipEntryBase entry, string newname) = pair.Value; 76 | if (entry.Type is EntryType.Archive) 77 | { 78 | newpath = ((ZipEntryFile)entry).ChangeName(newname); 79 | result[pair.Key] = newpath; 80 | continue; 81 | } 82 | 83 | ZipEntryDirectory dir = (ZipEntryDirectory)entry; 84 | newpath = dir.ChangeName(newname); 85 | result[pair.Key] = newpath; 86 | Regex re = new( 87 | Regex.Escape(dir.RelativePath), 88 | RegexOptions.Compiled | RegexOptions.IgnoreCase); 89 | 90 | foreach (ZipArchiveEntry key in dir.GetChilds(cache[dir.Source])) 91 | { 92 | string child = result.ContainsKey(key.FullName) 93 | ? result[key.FullName] 94 | : key.FullName; 95 | 96 | result[key.FullName] = re.Replace(child, newpath); 97 | } 98 | } 99 | 100 | return result; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/SetZipEntryContentCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Management.Automation; 5 | using System.Text; 6 | using PSCompression.Exceptions; 7 | using PSCompression.Extensions; 8 | 9 | namespace PSCompression.Commands; 10 | 11 | [Cmdlet(VerbsCommon.Set, "ZipEntryContent", DefaultParameterSetName = "StringValue")] 12 | [OutputType(typeof(ZipEntryFile))] 13 | [Alias("zipsc")] 14 | public sealed class SetZipEntryContentCommand : PSCmdlet, IDisposable 15 | { 16 | private ZipArchive? _zip; 17 | 18 | private ZipEntryByteWriter? _byteWriter; 19 | 20 | private StreamWriter? _stringWriter; 21 | 22 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 23 | public object[] Value { get; set; } = null!; 24 | 25 | [Parameter(Mandatory = true, Position = 0)] 26 | public ZipEntryFile SourceEntry { get; set; } = null!; 27 | 28 | [Parameter(ParameterSetName = "StringValue")] 29 | [ArgumentCompleter(typeof(EncodingCompleter))] 30 | [EncodingTransformation] 31 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 32 | 33 | [Parameter(ParameterSetName = "ByteStream")] 34 | public SwitchParameter AsByteStream { get; set; } 35 | 36 | [Parameter(ParameterSetName = "StringValue")] 37 | [Parameter(ParameterSetName = "ByteStream")] 38 | public SwitchParameter Append { get; set; } 39 | 40 | [Parameter(ParameterSetName = "ByteStream")] 41 | public int BufferSize { get; set; } = 128_000; 42 | 43 | [Parameter] 44 | public SwitchParameter PassThru { get; set; } 45 | 46 | protected override void BeginProcessing() 47 | { 48 | try 49 | { 50 | _zip = SourceEntry.OpenWrite(); 51 | Stream stream = SourceEntry.Open(_zip); 52 | 53 | if (AsByteStream) 54 | { 55 | _byteWriter = new ZipEntryByteWriter(stream, BufferSize, Append); 56 | return; 57 | } 58 | 59 | _stringWriter = new StreamWriter(stream, Encoding); 60 | 61 | if (Append) 62 | { 63 | _stringWriter.BaseStream.Seek(0, SeekOrigin.End); 64 | return; 65 | } 66 | 67 | _stringWriter.BaseStream.SetLength(0); 68 | } 69 | catch (Exception exception) 70 | { 71 | ThrowTerminatingError(exception.ToStreamOpenError(SourceEntry)); 72 | } 73 | } 74 | 75 | protected override void ProcessRecord() 76 | { 77 | try 78 | { 79 | if (AsByteStream) 80 | { 81 | _byteWriter!.WriteBytes(LanguagePrimitives.ConvertTo(Value)); 82 | return; 83 | } 84 | 85 | _stringWriter!.WriteLines(LanguagePrimitives.ConvertTo(Value)); 86 | } 87 | catch (Exception exception) 88 | { 89 | ThrowTerminatingError(exception.ToWriteError(SourceEntry)); 90 | } 91 | } 92 | 93 | protected override void EndProcessing() 94 | { 95 | if (!PassThru) return; 96 | 97 | try 98 | { 99 | Dispose(); 100 | SourceEntry.Refresh(); 101 | WriteObject(SourceEntry); 102 | } 103 | catch (Exception exception) 104 | { 105 | ThrowTerminatingError(exception.ToStreamOpenError(SourceEntry)); 106 | } 107 | } 108 | 109 | public void Dispose() 110 | { 111 | _byteWriter?.Dispose(); 112 | _stringWriter?.Dispose(); 113 | _zip?.Dispose(); 114 | GC.SuppressFinalize(this); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Pester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace ProjectBuilder; 9 | 10 | public sealed class Pester 11 | { 12 | public string? PesterScript 13 | { 14 | get 15 | { 16 | _pesterPath ??= Path.Combine( 17 | _info.Root.FullName, 18 | "tools", 19 | "PesterTest.ps1"); 20 | 21 | if (_testsExist = File.Exists(_pesterPath)) 22 | { 23 | return _pesterPath; 24 | } 25 | 26 | return null; 27 | } 28 | } 29 | 30 | public string? ResultPath { get => _testsExist ? Path.Combine(_info.Project.Build, "TestResults") : null; } 31 | 32 | public string? ResultFile { get => _testsExist ? Path.Combine(ResultPath, "Pester.xml") : null; } 33 | 34 | private readonly ProjectInfo _info; 35 | 36 | private string? _pesterPath; 37 | 38 | private bool _testsExist; 39 | 40 | internal Pester(ProjectInfo info) => _info = info; 41 | 42 | private void CreateResultPath() 43 | { 44 | if (!Directory.Exists(ResultPath)) 45 | { 46 | Directory.CreateDirectory(ResultPath); 47 | } 48 | } 49 | 50 | public void ClearResultFile() 51 | { 52 | if (File.Exists(ResultFile)) 53 | { 54 | File.Delete(ResultFile); 55 | } 56 | } 57 | 58 | public string[] GetTestArgs(Version version) 59 | { 60 | CreateResultPath(); 61 | 62 | List arguments = [ 63 | "-NoProfile", 64 | "-NonInteractive", 65 | ]; 66 | 67 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 68 | { 69 | arguments.AddRange(["-ExecutionPolicy", "Bypass"]); 70 | } 71 | 72 | arguments.AddRange([ 73 | "-File", PesterScript!, 74 | "-TestPath", Path.Combine(_info.Root.FullName, "tests"), 75 | "-OutputFile", ResultFile! 76 | ]); 77 | 78 | Regex re = new("^|$", RegexOptions.Compiled); 79 | string targetArgs = re.Replace(string.Join("\" \"", [.. arguments]), "\""); 80 | string pwsh = Regex.Replace(Environment.GetCommandLineArgs().First(), @"\.dll$", string.Empty); 81 | string unitCoveragePath = Path.Combine(ResultPath, "UnitCoverage.json"); 82 | string watchFolder = Path.Combine(_info.Project.Release, "bin", _info.Project.TestFramework); 83 | string sourceMappingFile = Path.Combine(ResultPath, "CoverageSourceMapping.txt"); 84 | 85 | if (version is not { Major: >= 7, Minor: > 0 }) 86 | { 87 | targetArgs = re.Replace(targetArgs, "\""); 88 | watchFolder = re.Replace(watchFolder, "\""); 89 | } 90 | 91 | arguments.Clear(); 92 | arguments.AddRange([ 93 | watchFolder, 94 | "--target", pwsh, 95 | "--targetargs", targetArgs, 96 | "--output", Path.Combine(ResultPath, "Coverage.xml"), 97 | "--format", "cobertura" 98 | ]); 99 | 100 | if (File.Exists(unitCoveragePath)) 101 | { 102 | arguments.AddRange(["--merge-with", unitCoveragePath]); 103 | } 104 | 105 | if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true") 106 | { 107 | arguments.AddRange(["--source-mapping-file", sourceMappingFile]); 108 | File.WriteAllText( 109 | sourceMappingFile, 110 | $"|{_info.Root.FullName}{Path.DirectorySeparatorChar}=/_/"); 111 | } 112 | 113 | return [.. arguments]; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /docs/en-US/ConvertFrom-BrotliString.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # ConvertFrom-BrotliString 9 | 10 | ## SYNOPSIS 11 | 12 | Decompresses Brotli-compressed Base64-encoded strings. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | ConvertFrom-BrotliString 18 | [-InputObject] 19 | [-Encoding ] 20 | [-Raw] 21 | [] 22 | ``` 23 | 24 | ## DESCRIPTION 25 | 26 | The `ConvertFrom-BrotliString` cmdlet decompresses Base64-encoded strings that were compressed using Brotli compression (via the `BrotliStream` class from the `BrotliSharpLib` library). It is the counterpart to [`ConvertTo-BrotliString`](./ConvertTo-BrotliString.md). 27 | 28 | ## EXAMPLES 29 | 30 | ### Example 1: Decompress a Brotli-compressed Base64 string 31 | 32 | ```powershell 33 | PS ..\pwsh> ConvertFrom-BrotliString CwiAaGVsbG8NCndvcmxkDQohDQoD 34 | 35 | hello 36 | world 37 | ! 38 | ``` 39 | 40 | This example decompresses a Brotli-compressed Base64 string, restoring the original multi-line text. 41 | 42 | ### Example 2: Compare default behavior with the `-Raw` switch 43 | 44 | ```powershell 45 | PS ..\pwsh> $strings = 'hello', 'world', '!' 46 | PS ..\pwsh> $compressed = $strings | ConvertTo-BrotliString 47 | PS ..\pwsh> $decompressed = $compressed | ConvertFrom-BrotliString -Raw 48 | PS ..\pwsh> $decompressed.GetType() # System.String 49 | PS ..\pwsh> $decompressed 50 | 51 | hello 52 | world 53 | ! 54 | ``` 55 | 56 | This example compares the default behavior (outputting an array of strings split on newlines) with the `-Raw` switch (returning a single string with newlines preserved). 57 | 58 | ## PARAMETERS 59 | 60 | ### -Encoding 61 | 62 | Specifies the text encoding to use for the decompressed output string(s). 63 | 64 | > [!NOTE] 65 | > The default encoding is UTF-8 without BOM. 66 | 67 | ```yaml 68 | Type: Encoding 69 | Parameter Sets: (All) 70 | Aliases: 71 | 72 | Required: False 73 | Position: Named 74 | Default value: utf8NoBOM 75 | Accept pipeline input: False 76 | Accept wildcard characters: False 77 | ``` 78 | 79 | ### -InputObject 80 | 81 | Specifies the input string or strings to expand. 82 | 83 | ```yaml 84 | Type: String[] 85 | Parameter Sets: (All) 86 | Aliases: 87 | 88 | Required: True 89 | Position: 0 90 | Default value: None 91 | Accept pipeline input: True (ByValue) 92 | Accept wildcard characters: False 93 | ``` 94 | 95 | ### -Raw 96 | 97 | By default, the cmdlet splits the decompressed text on newline characters and outputs an array of strings. The `-Raw` switch returns the entire decompressed text as a single string (with newlines preserved). 98 | 99 | ```yaml 100 | Type: SwitchParameter 101 | Parameter Sets: (All) 102 | Aliases: 103 | 104 | Required: False 105 | Position: Named 106 | Default value: False 107 | Accept pipeline input: False 108 | Accept wildcard characters: False 109 | ``` 110 | 111 | ### CommonParameters 112 | 113 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 114 | 115 | ## INPUTS 116 | 117 | ### System.String[] 118 | 119 | You can pipe Brotli Base64 strings to this cmdlet. 120 | 121 | ## OUTPUTS 122 | 123 | ### System.String 124 | 125 | By default: `System.String[]` (one element per line). With `-Raw`: `System.String` (single multi-line string). 126 | 127 | ## NOTES 128 | 129 | ## RELATED LINKS 130 | 131 | [__`ConvertTo-BrotliString`__](./ConvertTo-BrotliString.md) 132 | 133 | [__BrotliSharpLib__](https://github.com/master131/BrotliSharpLib) 134 | 135 | [__System.IO.Compression__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression) 136 | -------------------------------------------------------------------------------- /docs/en-US/ConvertFrom-GzipString.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # ConvertFrom-GzipString 9 | 10 | ## SYNOPSIS 11 | 12 | Decompresses Gzip-compressed Base64-encoded strings. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | ConvertFrom-GzipString 18 | [-InputObject] 19 | [-Encoding ] 20 | [-Raw] 21 | [] 22 | ``` 23 | 24 | ## DESCRIPTION 25 | 26 | The `ConvertFrom-GzipString` cmdlet decompresses Base64-encoded strings that were compressed using GZip compression (via the [`GZipStream` class](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.gzipstream)). It is the counterpart to [`ConvertTo-GzipString`](./ConvertTo-GzipString.md). 27 | 28 | ## EXAMPLES 29 | 30 | ### Example 1: Decompress a GZip-compressed Base64 string 31 | 32 | ```powershell 33 | PS ..\pwsh> ConvertFrom-GzipString H4sIAAAAAAAACstIzcnJ5+Uqzy/KSeHlUuTlAgBLr/K2EQAAAA== 34 | 35 | hello 36 | world 37 | ! 38 | ``` 39 | 40 | This example decompresses a GZip-compressed Base64 string, restoring the original multi-line text. 41 | 42 | ### Example 2: Compare default behavior with the `-Raw` switch 43 | 44 | ```powershell 45 | PS ..\pwsh> $strings = 'hello', 'world', '!' 46 | PS ..\pwsh> $compressed = $strings | ConvertTo-GzipString 47 | PS ..\pwsh> $decompressed = $compressed | ConvertFrom-GzipString -Raw 48 | PS ..\pwsh> $decompressed.GetType() # System.String 49 | PS ..\pwsh> $decompressed 50 | 51 | hello 52 | world 53 | ! 54 | ``` 55 | 56 | This example compares the default behavior (outputting an array of strings split on newlines) with the `-Raw` switch (returning a single string with newlines preserved). 57 | 58 | ## PARAMETERS 59 | 60 | ### -Encoding 61 | 62 | Specifies the text encoding to use for the decompressed output string(s). 63 | 64 | > [!NOTE] 65 | > The default encoding is UTF-8 without BOM. 66 | 67 | ```yaml 68 | Type: Encoding 69 | Parameter Sets: (All) 70 | Aliases: 71 | 72 | Required: False 73 | Position: Named 74 | Default value: utf8NoBOM 75 | Accept pipeline input: False 76 | Accept wildcard characters: False 77 | ``` 78 | 79 | ### -InputObject 80 | 81 | Specifies the GZip-compressed Base64 string(s) to decompress. 82 | 83 | ```yaml 84 | Type: String[] 85 | Parameter Sets: (All) 86 | Aliases: 87 | 88 | Required: True 89 | Position: 0 90 | Default value: None 91 | Accept pipeline input: True (ByValue) 92 | Accept wildcard characters: False 93 | ``` 94 | 95 | ### -Raw 96 | 97 | By default, the cmdlet splits the decompressed text on newline characters and outputs an array of strings. The `-Raw` switch returns the entire decompressed text as a single string (with newlines preserved). 98 | 99 | ```yaml 100 | Type: SwitchParameter 101 | Parameter Sets: (All) 102 | Aliases: 103 | 104 | Required: False 105 | Position: Named 106 | Default value: False 107 | Accept pipeline input: False 108 | Accept wildcard characters: False 109 | ``` 110 | 111 | ### CommonParameters 112 | 113 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 114 | 115 | ## INPUTS 116 | 117 | ### System.String[] 118 | 119 | You can pipe GZip-compressed Base64 strings to this cmdlet. 120 | 121 | ## OUTPUTS 122 | 123 | ### System.String 124 | 125 | By default: `System.String[]` (one element per line). With `-Raw`: `System.String` (single multi-line string). 126 | 127 | ## NOTES 128 | 129 | ## RELATED LINKS 130 | 131 | [__`ConvertTo-GzipString`__](./ConvertTo-GzipString.md) 132 | 133 | [__System.IO.Compression__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression) 134 | 135 | [__GzipStream Class__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.gzipstream) 136 | -------------------------------------------------------------------------------- /src/PSCompression/Extensions/PathExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using System.Runtime.CompilerServices; 5 | using System.Text.RegularExpressions; 6 | using Microsoft.PowerShell.Commands; 7 | using PSCompression.Exceptions; 8 | 9 | namespace PSCompression.Extensions; 10 | 11 | public static partial class PathExtensions 12 | { 13 | #if NETCOREAPP 14 | [GeneratedRegex( 15 | @"(?:^[a-z]:)?[\\/]+|(? 29 | path.EndsWith("/") || path.EndsWith("\\") 30 | ? NormalizeEntryPath(path) 31 | : NormalizeFileEntryPath(path); 32 | 33 | internal static string ResolvePath(this string path, PSCmdlet cmdlet) 34 | { 35 | string resolved = cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath( 36 | path: path, 37 | provider: out ProviderInfo provider, 38 | drive: out _); 39 | 40 | provider.Validate(path, throwOnInvalidProvider: true, cmdlet); 41 | return resolved; 42 | } 43 | 44 | internal static bool Validate( 45 | this ProviderInfo provider, 46 | string path, 47 | bool throwOnInvalidProvider, 48 | PSCmdlet cmdlet) 49 | { 50 | if (provider.ImplementingType == typeof(FileSystemProvider)) 51 | { 52 | return true; 53 | } 54 | 55 | ErrorRecord error = provider.ToInvalidProviderError(path); 56 | 57 | if (throwOnInvalidProvider) 58 | { 59 | cmdlet.ThrowTerminatingError(error); 60 | } 61 | 62 | cmdlet.WriteError(error); 63 | return false; 64 | } 65 | 66 | internal static string AddExtensionIfMissing(this string path, string extension) 67 | { 68 | if (!path.EndsWith(extension, StringComparison.InvariantCultureIgnoreCase)) 69 | { 70 | path += extension; 71 | } 72 | 73 | return path; 74 | } 75 | 76 | internal static string NormalizeEntryPath(this string path) 77 | => s_reNormalize 78 | .Replace(path, DirectorySeparator) 79 | .TrimStart('/'); 80 | 81 | internal static string NormalizeFileEntryPath(this string path) 82 | => NormalizeEntryPath(path).TrimEnd('/'); 83 | 84 | internal static void Create(this DirectoryInfo dir, bool force) 85 | { 86 | if (force || !dir.Exists) 87 | { 88 | dir.Create(); 89 | return; 90 | } 91 | 92 | throw new IOException($"The directory '{dir.FullName}' already exists."); 93 | } 94 | 95 | internal static PSObject AppendPSProperties(this FileSystemInfo info) 96 | { 97 | string? parent = info is DirectoryInfo dir 98 | ? dir.Parent?.FullName 99 | : Unsafe.As(info).DirectoryName; 100 | 101 | return info.AppendPSProperties(parent); 102 | } 103 | 104 | internal static PSObject AppendPSProperties(this FileSystemInfo info, string? parent) 105 | { 106 | const string Provider = @"Microsoft.PowerShell.Core\FileSystem::"; 107 | PSObject pso = PSObject.AsPSObject(info); 108 | pso.Properties.Add(new PSNoteProperty("PSPath", $"{Provider}{info.FullName}")); 109 | pso.Properties.Add(new PSNoteProperty("PSParentPath", $"{Provider}{parent}")); 110 | return pso; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /docs/en-US/ConvertFrom-DeflateString.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # ConvertFrom-DeflateString 9 | 10 | ## SYNOPSIS 11 | 12 | Decompresses Deflate-compressed Base64-encoded strings. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | ConvertFrom-DeflateString 18 | [-InputObject] 19 | [-Encoding ] 20 | [-Raw] 21 | [] 22 | ``` 23 | 24 | ## DESCRIPTION 25 | 26 | The ConvertFrom-DeflateString cmdlet decompresses Base64-encoded strings that were compressed using Deflate compression (via the [`DeflateStream` class](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream)). It is the counterpart to [`ConvertTo-DeflateString`](./ConvertTo-DeflateString.md). 27 | 28 | ## EXAMPLES 29 | 30 | ### Example 1: Decompress a Deflate-compressed Base64 string 31 | 32 | ```powershell 33 | PS ..\pwsh> ConvertFrom-DeflateString ykjNycnn5SrPL8pJ4eVS5OUCAAAA//8DAA== 34 | 35 | hello 36 | world 37 | ! 38 | ``` 39 | 40 | This example decompresses a Deflate-compressed Base64 string, restoring the original multi-line text. 41 | 42 | ### Example 2: Compare default behavior with the `-Raw` switch 43 | 44 | ```powershell 45 | PS ..\pwsh> $strings = 'hello', 'world', '!' 46 | PS ..\pwsh> $compressed = $strings | ConvertTo-DeflateString 47 | PS ..\pwsh> $decompressed = $compressed | ConvertFrom-DeflateString -Raw 48 | PS ..\pwsh> $decompressed.GetType() # System.String 49 | PS ..\pwsh> $decompressed 50 | 51 | hello 52 | world 53 | ! 54 | ``` 55 | 56 | This example compares the default behavior (outputting an array of strings split on newlines) with the `-Raw` switch (returning a single string with newlines preserved). 57 | 58 | ## PARAMETERS 59 | 60 | ### -Encoding 61 | 62 | Specifies the text encoding to use for the decompressed output string(s). 63 | 64 | > [!NOTE] 65 | > The default encoding is UTF-8 without BOM. 66 | 67 | ```yaml 68 | Type: Encoding 69 | Parameter Sets: (All) 70 | Aliases: 71 | 72 | Required: False 73 | Position: Named 74 | Default value: utf8NoBOM 75 | Accept pipeline input: False 76 | Accept wildcard characters: False 77 | ``` 78 | 79 | ### -InputObject 80 | 81 | Specifies the input string or strings to expand. 82 | 83 | ```yaml 84 | Type: String[] 85 | Parameter Sets: (All) 86 | Aliases: 87 | 88 | Required: True 89 | Position: 0 90 | Default value: None 91 | Accept pipeline input: True (ByValue) 92 | Accept wildcard characters: False 93 | ``` 94 | 95 | ### -Raw 96 | 97 | By default, the cmdlet splits the decompressed text on newline characters and outputs an array of strings. The `-Raw` switch returns the entire decompressed text as a single string (with newlines preserved). 98 | 99 | ```yaml 100 | Type: SwitchParameter 101 | Parameter Sets: (All) 102 | Aliases: 103 | 104 | Required: False 105 | Position: Named 106 | Default value: False 107 | Accept pipeline input: False 108 | Accept wildcard characters: False 109 | ``` 110 | 111 | ### CommonParameters 112 | 113 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 114 | 115 | ## INPUTS 116 | 117 | ### System.String[] 118 | 119 | You can pipe Deflate-compressed Base64 strings to this cmdlet. 120 | 121 | ## OUTPUTS 122 | 123 | ### System.String 124 | 125 | By default: `System.String[]` (one element per line). With `-Raw`: `System.String` (single multi-line string). 126 | 127 | ## NOTES 128 | 129 | ## RELATED LINKS 130 | 131 | [__`ConvertTo-DeflateString`__](./ConvertTo-DeflateString.md) 132 | 133 | [__System.IO.Compression__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression) 134 | 135 | [__DeflateStream Class__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream) 136 | -------------------------------------------------------------------------------- /docs/en-US/Remove-ZipEntry.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # Remove-ZipEntry 9 | 10 | ## SYNOPSIS 11 | 12 | Removes specified entries from zip archives. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | Remove-ZipEntry 18 | -InputObject 19 | [-WhatIf] 20 | [-Confirm] 21 | [] 22 | ``` 23 | 24 | ## DESCRIPTION 25 | 26 | The `Remove-ZipEntry` cmdlet removes `ZipEntryFile` or `ZipEntryDirectory` objects produced by [`Get-ZipEntry`](./Get-ZipEntry.md) or [`New-ZipEntry`](./New-ZipEntry.md) from their parent zip archives. 27 | 28 | ## EXAMPLES 29 | 30 | ### Example 1: Remove all Zip Archive Entries from a Zip Archive 31 | 32 | ```powershell 33 | PS ..pwsh\> Get-ZipEntry .\myZip.zip | Remove-ZipEntry 34 | ``` 35 | 36 | This example removes all entries from `myZip.zip`, effectively emptying the archive. 37 | 38 | ### Example 2: Remove all `.txt` Entries from a Zip Archive 39 | 40 | ```powershell 41 | PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include *.txt | Remove-ZipEntry 42 | ``` 43 | 44 | This example removes only the entries matching the `*.txt` pattern from `myZip.zip`. 45 | 46 | ### Example 3: Prompt for confirmation before removing entries 47 | 48 | ```powershell 49 | PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include *.txt | Remove-ZipEntry -Confirm 50 | 51 | Confirm 52 | Are you sure you want to perform this action? 53 | Performing the operation "Remove" on target "test/helloworld.txt". 54 | [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): 55 | ``` 56 | 57 | This example prompts for confirmation before removing each matching entry. The cmdlet supports [`ShouldProcess`](https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-shouldprocess): use `-Confirm` to prompt or `-WhatIf` to preview changes without applying them. 58 | 59 | ## PARAMETERS 60 | 61 | ### -InputObject 62 | 63 | Specifies the zip entries to remove. This parameter accepts pipeline input from `Get-ZipEntry` or `New-ZipEntry` and can also be used as a named argument. 64 | 65 | ```yaml 66 | Type: ZipEntryBase[] 67 | Parameter Sets: (All) 68 | Aliases: 69 | 70 | Required: True 71 | Position: Named 72 | Default value: None 73 | Accept pipeline input: True (ByValue) 74 | Accept wildcard characters: False 75 | ``` 76 | 77 | ### -Confirm 78 | 79 | Prompts you for confirmation before running the cmdlet. 80 | 81 | ```yaml 82 | Type: SwitchParameter 83 | Parameter Sets: (All) 84 | Aliases: cf 85 | 86 | Required: False 87 | Position: Named 88 | Default value: None 89 | Accept pipeline input: False 90 | Accept wildcard characters: False 91 | ``` 92 | 93 | ### -WhatIf 94 | 95 | Shows what would happen if the cmdlet runs. The cmdlet is not run. 96 | 97 | ```yaml 98 | Type: SwitchParameter 99 | Parameter Sets: (All) 100 | Aliases: wi 101 | 102 | Required: False 103 | Position: Named 104 | Default value: None 105 | Accept pipeline input: False 106 | Accept wildcard characters: False 107 | ``` 108 | 109 | ### CommonParameters 110 | 111 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 112 | 113 | ## INPUTS 114 | 115 | ### PSCompression.Abstractions.ZipEntryBase[] 116 | 117 | You can pipe one or more `ZipEntryFile` or `ZipEntryDirectory` objects produced by [`Get-ZipEntry`](./Get-ZipEntry.md) or [`New-ZipEntry`](./New-ZipEntry.md) to this cmdlet. 118 | 119 | ## OUTPUTS 120 | 121 | ### None 122 | 123 | This cmdlet produces no output. 124 | 125 | ## NOTES 126 | 127 | ## RELATED LINKS 128 | 129 | [__`Get-ZipEntry`__](./Get-ZipEntry.md) 130 | 131 | [__`New-ZipEntry`__](./New-ZipEntry.md) 132 | 133 | [__System.IO.Compression.ZipArchive__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.ziparchive) 134 | 135 | [__System.IO.Compression.ZipArchiveEntry__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.ziparchiveentry) 136 | -------------------------------------------------------------------------------- /docs/en-US/ConvertFrom-ZLibString.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # ConvertFrom-ZLibString 9 | 10 | ## SYNOPSIS 11 | 12 | Decompresses ZLib-compressed Base64-encoded strings. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | ConvertFrom-ZLibString 18 | [-InputObject] 19 | [-Encoding ] 20 | [-Raw] 21 | [] 22 | ``` 23 | 24 | ## DESCRIPTION 25 | 26 | The `ConvertFrom-ZLibString` cmdlet decompresses Base64-encoded strings that were compressed using ZLib compression. It uses a custom ZLib implementation built on top of the [`DeflateStream` class](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream). For implementation details, see the PSCompression source code. 27 | This cmdlet is the counterpart of [`ConvertTo-ZLibString`](./ConvertTo-ZLibString.md)." 28 | 29 | ## EXAMPLES 30 | 31 | ### Example 1: Decompress a ZLib-compressed Base64 string 32 | 33 | ```powershell 34 | PS ..\pwsh> ConvertFrom-ZLibString eJzKSM3JyeflKs8vyknh5VLk5QIAAAD//wMAMosEow== 35 | 36 | hello 37 | world 38 | ! 39 | ``` 40 | 41 | This example decompresses a ZLib-compressed Base64 string, restoring the original multi-line text. 42 | 43 | ### Example 2: Compare default behavior with the `-Raw` switch 44 | 45 | ```powershell 46 | PS ..\pwsh> $strings = 'hello', 'world', '!' 47 | PS ..\pwsh> $compressed = $strings | ConvertTo-ZlibString 48 | PS ..\pwsh> $decompressed = $compressed | ConvertFrom-ZlibString -Raw 49 | PS ..\pwsh> $decompressed.GetType() # System.String 50 | PS ..\pwsh> $decompressed 51 | 52 | hello 53 | world 54 | ! 55 | ``` 56 | 57 | This example compares the default behavior (outputting an array of strings split on newlines) with the `-Raw` switch (returning a single string with newlines preserved). 58 | 59 | ## PARAMETERS 60 | 61 | ### -Encoding 62 | 63 | Specifies the text encoding to use for the decompressed output string(s). 64 | 65 | > [!NOTE] 66 | > The default encoding is UTF-8 without BOM. 67 | 68 | ```yaml 69 | Type: Encoding 70 | Parameter Sets: (All) 71 | Aliases: 72 | 73 | Required: False 74 | Position: Named 75 | Default value: utf8NoBOM 76 | Accept pipeline input: False 77 | Accept wildcard characters: False 78 | ``` 79 | 80 | ### -InputObject 81 | 82 | Specifies the ZLib-compressed Base64 string(s) to decompress. 83 | 84 | ```yaml 85 | Type: String[] 86 | Parameter Sets: (All) 87 | Aliases: 88 | 89 | Required: True 90 | Position: 0 91 | Default value: None 92 | Accept pipeline input: True (ByValue) 93 | Accept wildcard characters: False 94 | ``` 95 | 96 | ### -Raw 97 | 98 | By default, the cmdlet splits the decompressed text on newline characters and outputs an array of strings. The `-Raw` switch returns the entire decompressed text as a single string (with newlines preserved). 99 | 100 | ```yaml 101 | Type: SwitchParameter 102 | Parameter Sets: (All) 103 | Aliases: 104 | 105 | Required: False 106 | Position: Named 107 | Default value: False 108 | Accept pipeline input: False 109 | Accept wildcard characters: False 110 | ``` 111 | 112 | ### CommonParameters 113 | 114 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 115 | 116 | ## INPUTS 117 | 118 | ### System.String[] 119 | 120 | You can pipe ZLib-compressed Base64 strings to this cmdlet. 121 | 122 | ## OUTPUTS 123 | 124 | ### System.String 125 | 126 | By default: `System.String[]` (one element per line). With `-Raw`: `System.String` (single multi-line string). 127 | 128 | ## NOTES 129 | 130 | ## RELATED LINKS 131 | 132 | [__`ConvertTo-ZLibString`__](./ConvertTo-ZLibString.md) 133 | 134 | [__System.IO.Compression__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression) 135 | 136 | [__DeflateStream Class__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream) 137 | 138 | [__ZlibStream Class__](https://github.com/santisq/PSCompression/blob/main/src/PSCompression/ZlibStream.cs) 139 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/RenameZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Compression; 4 | using System.Management.Automation; 5 | using PSCompression.Abstractions; 6 | using PSCompression.Exceptions; 7 | using PSCompression.Extensions; 8 | 9 | namespace PSCompression.Commands; 10 | 11 | [Cmdlet(VerbsCommon.Rename, "ZipEntry", SupportsShouldProcess = true)] 12 | [OutputType(typeof(ZipEntryFile), typeof(ZipEntryDirectory))] 13 | [Alias("zipren")] 14 | public sealed class RenameZipEntryCommand : PSCmdlet, IDisposable 15 | { 16 | private readonly ZipArchiveCache _zipArchiveCache = new( 17 | entry => entry.OpenZip(ZipArchiveMode.Update)); 18 | 19 | private ZipEntryCache? _zipEntryCache; 20 | 21 | private readonly ZipEntryMoveCache _moveCache = new(); 22 | 23 | [Parameter( 24 | Mandatory = true, 25 | Position = 0, 26 | ValueFromPipeline = true)] 27 | public ZipEntryBase ZipEntry { get; set; } = null!; 28 | 29 | [Parameter( 30 | Mandatory = true, 31 | Position = 1, 32 | ValueFromPipeline = true)] 33 | public string NewName { get; set; } = null!; 34 | 35 | [Parameter] 36 | public SwitchParameter PassThru { get; set; } 37 | 38 | protected override void BeginProcessing() 39 | { 40 | if (PassThru.IsPresent) 41 | { 42 | _zipEntryCache = new(); 43 | } 44 | } 45 | 46 | protected override void ProcessRecord() 47 | { 48 | if (!ShouldProcess(target: ZipEntry.ToString(), action: "Rename")) 49 | { 50 | return; 51 | } 52 | 53 | try 54 | { 55 | ZipEntry.ThrowIfFromStream(); 56 | NewName.ThrowIfInvalidNameChar(); 57 | _zipArchiveCache.TryAdd(ZipEntry); 58 | _moveCache.AddEntry(ZipEntry, NewName); 59 | } 60 | catch (NotSupportedException exception) 61 | { 62 | ThrowTerminatingError(exception.ToStreamOpenError(ZipEntry)); 63 | } 64 | catch (InvalidNameException exception) 65 | { 66 | WriteError(exception.ToInvalidNameError(NewName)); 67 | } 68 | catch (Exception exception) 69 | { 70 | WriteError(exception.ToOpenError(ZipEntry.Source)); 71 | } 72 | } 73 | 74 | protected override void EndProcessing() 75 | { 76 | foreach (var mapping in _moveCache.GetMappings(_zipArchiveCache)) 77 | { 78 | Rename(mapping); 79 | } 80 | 81 | _zipArchiveCache?.Dispose(); 82 | if (!PassThru.IsPresent || _zipEntryCache is null) 83 | { 84 | return; 85 | } 86 | 87 | WriteObject( 88 | _zipEntryCache 89 | .AddRange(_moveCache.GetPassThruMappings()) 90 | .GetEntries() 91 | .ToEntrySort(), 92 | enumerateCollection: true); 93 | } 94 | 95 | private void Rename(KeyValuePair> mapping) 96 | { 97 | foreach ((string source, string destination) in mapping.Value) 98 | { 99 | try 100 | { 101 | ZipEntryBase.Move( 102 | sourceRelativePath: source, 103 | destination: destination, 104 | sourceZipPath: mapping.Key, 105 | _zipArchiveCache[mapping.Key]); 106 | } 107 | catch (DuplicatedEntryException exception) 108 | { 109 | if (_moveCache.IsDirectoryEntry(mapping.Key, source)) 110 | { 111 | ThrowTerminatingError(exception.ToDuplicatedEntryError()); 112 | } 113 | 114 | WriteError(exception.ToDuplicatedEntryError()); 115 | } 116 | catch (EntryNotFoundException exception) 117 | { 118 | WriteError(exception.ToEntryNotFoundError()); 119 | } 120 | catch (Exception exception) 121 | { 122 | WriteError(exception.ToWriteError(ZipEntry)); 123 | } 124 | } 125 | } 126 | 127 | public void Dispose() 128 | { 129 | _zipArchiveCache?.Dispose(); 130 | GC.SuppressFinalize(this); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ExpandTarArchiveCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Management.Automation; 6 | using System.Text; 7 | using ICSharpCode.SharpZipLib.Tar; 8 | using PSCompression.Abstractions; 9 | using PSCompression.Exceptions; 10 | using PSCompression.Extensions; 11 | using IO = System.IO; 12 | 13 | namespace PSCompression.Commands; 14 | 15 | [Cmdlet(VerbsData.Expand, "TarArchive")] 16 | [OutputType(typeof(FileInfo), typeof(DirectoryInfo))] 17 | [Alias("untar")] 18 | public sealed class ExpandTarArchiveCommand : CommandWithPathBase 19 | { 20 | private bool _shouldInferAlgo; 21 | 22 | [Parameter(Position = 1)] 23 | public string? Destination { get; set; } 24 | 25 | [Parameter] 26 | public Algorithm Algorithm { get; set; } 27 | 28 | [Parameter] 29 | public SwitchParameter Force { get; set; } 30 | 31 | [Parameter] 32 | public SwitchParameter PassThru { get; set; } 33 | 34 | protected override void BeginProcessing() 35 | { 36 | Destination = Destination is null 37 | // PowerShell is retarded and decided to mix up ProviderPath & Path 38 | ? SessionState.Path.CurrentFileSystemLocation.ProviderPath 39 | : Destination.ResolvePath(this); 40 | 41 | if (File.Exists(Destination)) 42 | { 43 | ThrowTerminatingError(ExceptionHelper.NotDirectoryPath( 44 | Destination, nameof(Destination))); 45 | } 46 | 47 | Directory.CreateDirectory(Destination); 48 | 49 | _shouldInferAlgo = !MyInvocation.BoundParameters 50 | .ContainsKey(nameof(Algorithm)); 51 | } 52 | 53 | protected override void ProcessRecord() 54 | { 55 | Dbg.Assert(Destination is not null); 56 | 57 | foreach (string path in EnumerateResolvedPaths()) 58 | { 59 | if (_shouldInferAlgo) 60 | { 61 | Algorithm = AlgorithmMappings.Parse(path); 62 | } 63 | 64 | try 65 | { 66 | FileSystemInfo[] output = ExtractArchive(path); 67 | 68 | if (PassThru) 69 | { 70 | IOrderedEnumerable result = output 71 | .Select(PathExtensions.AppendPSProperties) 72 | .OrderBy(pso => pso.Properties["PSParentPath"].Value); 73 | 74 | WriteObject(result, enumerateCollection: true); 75 | } 76 | } 77 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 78 | { 79 | throw; 80 | } 81 | catch (Exception exception) 82 | { 83 | WriteError(exception.ToWriteError(path)); 84 | } 85 | } 86 | } 87 | 88 | private FileSystemInfo[] ExtractArchive(string path) 89 | { 90 | using FileStream fs = File.OpenRead(path); 91 | using Stream decompress = Algorithm.FromCompressedStream(fs); 92 | using TarInputStream tar = new(decompress, Encoding.UTF8); 93 | 94 | List result = []; 95 | foreach (TarEntry entry in tar.EnumerateEntries()) 96 | { 97 | try 98 | { 99 | result.Add(ExtractEntry(entry, tar)); 100 | } 101 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 102 | { 103 | throw; 104 | } 105 | catch (Exception exception) 106 | { 107 | WriteError(exception.ToExtractEntryError(entry)); 108 | } 109 | } 110 | 111 | return [.. result]; 112 | } 113 | 114 | private FileSystemInfo ExtractEntry(TarEntry entry, TarInputStream tar) 115 | { 116 | Dbg.Assert(Destination is not null); 117 | 118 | string destination = IO.Path.GetFullPath( 119 | IO.Path.Combine(Destination, entry.Name)); 120 | 121 | if (entry.IsDirectory) 122 | { 123 | DirectoryInfo dir = new(destination); 124 | dir.Create(Force); 125 | return dir; 126 | } 127 | 128 | FileInfo file = new(destination); 129 | file.Directory?.Create(); 130 | 131 | using (FileStream destStream = File.Open( 132 | destination, 133 | Force ? FileMode.Create : FileMode.CreateNew, 134 | FileAccess.Write)) 135 | { 136 | if (entry.Size > 0) 137 | { 138 | tar.CopyTo(destStream, (int)entry.Size); 139 | } 140 | } 141 | 142 | return file; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: PSCompression Workflow 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | release: 12 | types: 13 | - published 14 | 15 | env: 16 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 17 | POWERSHELL_TELEMETRY_OPTOUT: 1 18 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 19 | DOTNET_NOLOGO: true 20 | BUILD_CONFIGURATION: ${{ fromJSON('["Debug", "Release"]')[startsWith(github.ref, 'refs/tags/v')] }} 21 | 22 | jobs: 23 | build: 24 | name: build 25 | runs-on: windows-latest 26 | steps: 27 | - name: Check out repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Build module - Debug 31 | shell: pwsh 32 | run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Build 33 | if: ${{ env.BUILD_CONFIGURATION == 'Debug' }} 34 | 35 | - name: Build module - Publish 36 | shell: pwsh 37 | run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Build 38 | if: ${{ env.BUILD_CONFIGURATION == 'Release' }} 39 | 40 | - name: Capture PowerShell Module 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: PSModule 44 | path: output/*.nupkg 45 | 46 | test: 47 | name: test 48 | needs: 49 | - build 50 | runs-on: ${{ matrix.info.os }} 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | info: 55 | - name: PS_5.1 56 | psversion: '5.1' 57 | os: windows-latest 58 | - name: PS_7_Windows 59 | psversion: '7' 60 | os: windows-latest 61 | - name: PS_7_Linux 62 | psversion: '7' 63 | os: ubuntu-latest 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | 68 | - name: Restore Built PowerShell Module 69 | uses: actions/download-artifact@v4 70 | with: 71 | name: PSModule 72 | path: output 73 | 74 | - name: Install Built PowerShell Module 75 | shell: pwsh 76 | run: | 77 | $manifestItem = Get-Item ([IO.Path]::Combine('module', '*.psd1')) 78 | $moduleName = $manifestItem.BaseName 79 | $manifest = Test-ModuleManifest -Path $manifestItem.FullName -ErrorAction SilentlyContinue -WarningAction Ignore 80 | 81 | $destPath = [IO.Path]::Combine('output', $moduleName, $manifest.Version) 82 | if (-not (Test-Path -LiteralPath $destPath)) { 83 | New-Item -Path $destPath -ItemType Directory | Out-Null 84 | } 85 | 86 | Get-ChildItem output/*.nupkg | Rename-Item -NewName { $_.Name -replace '.nupkg', '.zip' } 87 | 88 | Expand-Archive -Path output/*.zip -DestinationPath $destPath -Force -ErrorAction Stop 89 | 90 | - name: Run Tests - Windows PowerShell 91 | if: ${{ matrix.info.psversion == '5.1' }} 92 | shell: pwsh 93 | run: | 94 | powershell.exe -NoProfile -File ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test 95 | exit $LASTEXITCODE 96 | 97 | - name: Run Tests - PowerShell 98 | if: ${{ matrix.info.psversion != '5.1' }} 99 | shell: pwsh 100 | run: | 101 | pwsh -NoProfile -File ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test 102 | exit $LASTEXITCODE 103 | 104 | - name: Upload Test Results 105 | if: always() 106 | uses: actions/upload-artifact@v4 107 | with: 108 | name: Unit Test Results (${{ matrix.info.name }}) 109 | path: ./output/TestResults/Pester.xml 110 | 111 | - name: Upload Coverage Results 112 | if: always() && !startsWith(github.ref, 'refs/tags/v') 113 | uses: actions/upload-artifact@v4 114 | with: 115 | name: Coverage Results (${{ matrix.info.name }}) 116 | path: ./output/TestResults/Coverage.xml 117 | 118 | - name: Upload Coverage to codecov 119 | if: always() && !startsWith(github.ref, 'refs/tags/v') 120 | uses: codecov/codecov-action@v4 121 | with: 122 | files: ./output/TestResults/Coverage.xml 123 | flags: ${{ matrix.info.name }} 124 | token: ${{ secrets.CODECOV_TOKEN }} 125 | 126 | publish: 127 | name: publish 128 | if: startsWith(github.ref, 'refs/tags/v') 129 | needs: 130 | - build 131 | - test 132 | runs-on: windows-latest 133 | steps: 134 | - name: Restore Built PowerShell Module 135 | uses: actions/download-artifact@v4 136 | with: 137 | name: PSModule 138 | path: ./ 139 | 140 | - name: Publish to Gallery 141 | if: github.event_name == 'release' 142 | shell: pwsh 143 | run: >- 144 | dotnet nuget push '*.nupkg' 145 | --api-key $env:PSGALLERY_TOKEN 146 | --source 'https://www.powershellgallery.com/api/v2/package' 147 | --no-symbols 148 | env: 149 | PSGALLERY_TOKEN: ${{ secrets.PSGALLERY_TOKEN }} 150 | -------------------------------------------------------------------------------- /module/PSCompression.Format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | zipentryview 6 | 7 | PSCompression.Abstractions.ZipEntryBase 8 | 9 | 10 | [PSCompression.Internal._Format]::GetDirectoryPath($_) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 10 18 | Left 19 | 20 | 21 | 22 | 26 23 | Right 24 | 25 | 26 | 27 | 15 28 | Right 29 | 30 | 31 | 32 | 15 33 | Right 34 | 35 | 36 | 37 | Left 38 | 39 | 40 | 41 | 42 | 43 | 44 | Type 45 | 46 | 47 | [PSCompression.Internal._Format]::GetFormattedDate($_.LastWriteTime) 48 | 49 | 50 | 51 | if ($_ -is [PSCompression.ZipEntryFile]) { 52 | [PSCompression.Internal._Format]::GetFormattedLength($_.CompressedLength) 53 | } 54 | 55 | 56 | 57 | 58 | if ($_ -is [PSCompression.ZipEntryFile]) { 59 | [PSCompression.Internal._Format]::GetFormattedLength($_.Length) 60 | } 61 | 62 | 63 | 64 | Name 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | tarentryview 73 | 74 | PSCompression.Abstractions.TarEntryBase 75 | 76 | 77 | [PSCompression.Internal._Format]::GetDirectoryPath($_) 78 | 79 | 80 | 81 | 82 | 83 | 84 | 10 85 | Left 86 | 87 | 88 | 89 | 26 90 | Right 91 | 92 | 93 | 94 | 15 95 | Right 96 | 97 | 98 | 99 | Left 100 | 101 | 102 | 103 | 104 | 105 | 106 | Type 107 | 108 | 109 | [PSCompression.Internal._Format]::GetFormattedDate($_.LastWriteTime) 110 | 111 | 112 | 113 | if ($_ -is [PSCompression.TarEntryFile]) { 114 | [PSCompression.Internal._Format]::GetFormattedLength($_.Length) 115 | } 116 | 117 | 118 | 119 | Name 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /tests/ZipEntryBase.tests.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | using namespace System.IO.Compression 3 | 4 | $ErrorActionPreference = 'Stop' 5 | 6 | $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 7 | $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 8 | 9 | Import-Module $manifestPath 10 | Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) 11 | 12 | Describe 'ZipEntryBase Class' { 13 | BeforeAll { 14 | $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 15 | 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt 16 | New-ZipEntry $zip.FullName -EntryPath somefolder/ 17 | $encryptedZip = Get-Item $PSScriptRoot/../assets/helloworld.zip 18 | $encryptedZip | Out-Null 19 | } 20 | 21 | It 'Can extract an entry' { 22 | ($zip | Get-ZipEntry -Type Archive).ExtractTo($TestDrive, $false) | 23 | Should -BeOfType ([FileInfo]) 24 | } 25 | 26 | It 'Can overwrite a file when extracting' { 27 | ($zip | Get-ZipEntry -Type Archive).ExtractTo($TestDrive, $true) | 28 | Should -BeOfType ([FileInfo]) 29 | } 30 | 31 | It 'Can extract a file from entries created from input Stream' { 32 | Use-Object ($stream = $zip.OpenRead()) { 33 | ($stream | Get-ZipEntry -Type Archive).ExtractTo($TestDrive, $true) 34 | } | Should -BeOfType ([FileInfo]) 35 | } 36 | 37 | It 'Can create a new folder in the destination path when extracting' { 38 | $entry = $zip | Get-ZipEntry -Type Archive 39 | $file = $entry.ExtractTo([Path]::Combine($TestDrive, 'myTestFolder'), $false) 40 | 41 | $file.FullName | Should -BeExactly ([Path]::Combine($TestDrive, 'myTestFolder', $entry.Name)) 42 | } 43 | 44 | It 'Can extract an encrypted entry' { 45 | $passw = ConvertTo-SecureString 'test' -AsPlainText -Force 46 | $dest = Join-Path $TestDrive encryptedTestFolder 47 | Use-Object ($stream = $encryptedZip.OpenRead()) { 48 | $info = ($stream | Get-ZipEntry -Type Archive).ExtractTo($dest, $false, $passw) 49 | $info | Should -BeOfType ([FileInfo]) 50 | Get-Content $info.FullName | Should -BeExactly 'hello world!' 51 | $info.Delete() 52 | } 53 | 54 | $info = ($encryptedZip | Get-ZipEntry).ExtractTo($dest, $false, $passw) 55 | $info | Should -BeOfType ([FileInfo]) 56 | Get-Content $info.FullName | Should -BeExactly 'hello world!' 57 | $info.Delete() 58 | } 59 | 60 | It 'Can extract folders' { 61 | ($zip | Get-ZipEntry -Type Directory).ExtractTo($TestDrive, $false) | 62 | Should -BeOfType ([DirectoryInfo]) 63 | } 64 | 65 | It 'Can overwrite folders when extracting' { 66 | ($zip | Get-ZipEntry -Type Directory).ExtractTo($TestDrive, $true) | 67 | Should -BeOfType ([DirectoryInfo]) 68 | } 69 | 70 | It 'Has a LastWriteTime Property' { 71 | ($zip | Get-ZipEntry).LastWriteTime | Should -BeOfType ([datetime]) 72 | } 73 | 74 | It 'Has a CompressionRatio Property' { 75 | New-ZipEntry $zip.FullName -EntryPath empty.txt 76 | ($zip | Get-ZipEntry).CompressionRatio | Should -BeOfType ([string]) 77 | } 78 | 79 | It 'Has a Crc Property' { 80 | ($zip | Get-ZipEntry).Crc | Should -BeOfType ([long]) 81 | } 82 | 83 | It 'Can remove an entry in the source zip' { 84 | { $zip | Get-ZipEntry | ForEach-Object Remove } | 85 | Should -Not -Throw 86 | 87 | $zip | Get-ZipEntry | Should -BeNullOrEmpty 88 | } 89 | 90 | It 'Should throw if Remove() is used on entries created from input Stream' { 91 | 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt 92 | 93 | { 94 | Use-Object ($stream = $zip.OpenRead()) { 95 | $stream | Get-ZipEntry -Type Archive | ForEach-Object Remove 96 | } 97 | } | Should -Throw 98 | } 99 | 100 | It 'Opens a ZipArchive on OpenRead() and OpenWrite()' { 101 | Use-Object ($archive = ($zip | Get-ZipEntry).OpenRead()) { 102 | $archive | Should -BeOfType ([ZipArchive]) 103 | } 104 | 105 | Use-Object ($stream = $zip.OpenRead()) { 106 | Use-Object ($archive = ($stream | Get-ZipEntry).OpenRead()) { 107 | $archive | Should -BeOfType ([ZipArchive]) 108 | } 109 | } 110 | 111 | Use-Object ($archive = ($zip | Get-ZipEntry).OpenWrite()) { 112 | $archive | Should -BeOfType ([ZipArchive]) 113 | } 114 | } 115 | 116 | It 'Should throw if calling OpenWrite() on entries created from input Stream' { 117 | Use-Object ($stream = $zip.OpenRead()) { 118 | { 119 | Use-Object ($stream | Get-ZipEntry).OpenWrite() { } 120 | } | Should -Throw 121 | } 122 | } 123 | 124 | It 'Should throw if the entry to extract no longer exists in the source zip' { 125 | $entry = New-ZipEntry $zip.FullName -EntryPath toberemoved.txt 126 | $entry | Remove-ZipEntry 127 | { 128 | $entry.ExtractTo($TestDrive, $false) 129 | } | Should -Throw 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Module.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Linq; 7 | using System.Management.Automation; 8 | using System.Net.Http; 9 | using System.Threading.Tasks; 10 | 11 | namespace ProjectBuilder; 12 | 13 | public sealed class Module 14 | { 15 | public DirectoryInfo Root { get; } 16 | 17 | public FileInfo? Manifest { get; internal set; } 18 | 19 | public Version? Version { get; internal set; } 20 | 21 | public string Name { get; } 22 | 23 | public string PreRequisitePath { get; } 24 | 25 | private string? Release { get => _info.Project.Release; } 26 | 27 | private readonly UriBuilder _builder = new(_base); 28 | 29 | private const string _base = "https://www.powershellgallery.com"; 30 | 31 | private const string _path = "api/v2/package/{0}/{1}"; 32 | 33 | private readonly ProjectInfo _info; 34 | 35 | private Hashtable? _req; 36 | 37 | internal Module( 38 | DirectoryInfo directory, 39 | string name, 40 | ProjectInfo info) 41 | { 42 | Root = directory; 43 | Name = name; 44 | PreRequisitePath = InitPrerequisitePath(Root); 45 | _info = info; 46 | } 47 | 48 | public void CopyToRelease() => Root.CopyRecursive(Release); 49 | 50 | internal IEnumerable GetRequirements(string path) 51 | { 52 | _req ??= ImportRequirements(path); 53 | 54 | if (_req is { Count: 0 }) 55 | { 56 | return []; 57 | } 58 | 59 | List modules = new(_req.Count); 60 | foreach (DictionaryEntry entry in _req) 61 | { 62 | modules.Add(new ModuleDownload 63 | { 64 | Module = entry.Key.ToString(), 65 | Version = LanguagePrimitives.ConvertTo(entry.Value) 66 | }); 67 | } 68 | 69 | return DownloadModules([.. modules]); 70 | } 71 | 72 | private static Hashtable ImportRequirements(string path) 73 | { 74 | using PowerShell powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace); 75 | return powerShell 76 | .AddCommand("Import-PowerShellDataFile") 77 | .AddArgument(path) 78 | .Invoke() 79 | .FirstOrDefault(); 80 | } 81 | 82 | private string[] DownloadModules(ModuleDownload[] modules) 83 | { 84 | List> tasks = new(modules.Length); 85 | List output = new(modules.Length); 86 | 87 | foreach ((string module, Version version) in modules) 88 | { 89 | string destination = GetDestination(module); 90 | string modulePath = GetModulePath(module); 91 | 92 | if (Directory.Exists(modulePath)) 93 | { 94 | output.Add(modulePath); 95 | continue; 96 | } 97 | 98 | Console.WriteLine($"Installing build pre-req '{module}'"); 99 | _builder.Path = string.Format(_path, module, version); 100 | Task task = GetModuleAsync( 101 | uri: _builder.Uri.ToString(), 102 | destination: destination, 103 | expandPath: modulePath); 104 | tasks.Add(task); 105 | } 106 | 107 | output.AddRange(WaitTask(tasks)); 108 | return [.. output]; 109 | } 110 | 111 | private static string[] WaitTask(List> tasks) => 112 | WaitTaskAsync(tasks).GetAwaiter().GetResult(); 113 | 114 | private static void ExpandArchive(string source, string destination) => 115 | ZipFile.ExtractToDirectory(source, destination); 116 | 117 | private static async Task WaitTaskAsync( 118 | List> tasks) 119 | { 120 | List completedTasks = new(tasks.Count); 121 | while (tasks.Count > 0) 122 | { 123 | Task awaiter = await Task.WhenAny(tasks); 124 | tasks.Remove(awaiter); 125 | string module = await awaiter; 126 | completedTasks.Add(module); 127 | } 128 | return [.. completedTasks]; 129 | } 130 | 131 | private string GetDestination(string module) => 132 | Path.Combine(PreRequisitePath, Path.ChangeExtension(module, "zip")); 133 | 134 | private string GetModulePath(string module) => 135 | Path.Combine(PreRequisitePath, module); 136 | 137 | private static async Task GetModuleAsync( 138 | string uri, 139 | string destination, 140 | string expandPath) 141 | { 142 | using (FileStream fs = File.Create(destination)) 143 | { 144 | using HttpClient client = new(); 145 | using Stream stream = await client.GetStreamAsync(uri); 146 | await stream.CopyToAsync(fs); 147 | } 148 | 149 | ExpandArchive(destination, expandPath); 150 | File.Delete(destination); 151 | return expandPath; 152 | } 153 | 154 | private static string InitPrerequisitePath(DirectoryInfo root) 155 | { 156 | string path = Path.Combine(root.Parent.FullName, "tools", "Modules"); 157 | if (!Directory.Exists(path)) 158 | { 159 | Directory.CreateDirectory(path); 160 | } 161 | return path; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/PSCompression/Abstractions/GetEntryCommandBase.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Management.Automation; 3 | using System.IO; 4 | using System.ComponentModel; 5 | using System.Collections.Generic; 6 | using System; 7 | using PSCompression.Exceptions; 8 | using ICSharpCode.SharpZipLib.Tar; 9 | using ZstdSharp; 10 | using ICSharpCode.SharpZipLib.Zip; 11 | 12 | namespace PSCompression.Abstractions; 13 | 14 | [EditorBrowsable(EditorBrowsableState.Never)] 15 | public abstract class GetEntryCommandBase : CommandWithPathBase 16 | { 17 | internal abstract ArchiveType ArchiveType { get; } 18 | 19 | [Parameter( 20 | ParameterSetName = "Stream", 21 | Position = 0, 22 | Mandatory = true, 23 | ValueFromPipeline = true, 24 | ValueFromPipelineByPropertyName = true)] 25 | [Alias("RawContentStream")] 26 | public Stream? InputStream { get; set; } 27 | 28 | private WildcardPattern[]? _includePatterns; 29 | 30 | private WildcardPattern[]? _excludePatterns; 31 | 32 | [Parameter] 33 | public EntryType? Type { get; set; } 34 | 35 | [Parameter] 36 | [SupportsWildcards] 37 | public string[]? Include { get; set; } 38 | 39 | [Parameter] 40 | [SupportsWildcards] 41 | public string[]? Exclude { get; set; } 42 | 43 | protected override void BeginProcessing() 44 | { 45 | if (Exclude is null && Include is null) 46 | { 47 | return; 48 | } 49 | 50 | const WildcardOptions Options = WildcardOptions.Compiled 51 | | WildcardOptions.CultureInvariant 52 | | WildcardOptions.IgnoreCase; 53 | 54 | if (Exclude is not null) 55 | { 56 | _excludePatterns = [.. Exclude.Select(e => new WildcardPattern(e, Options))]; 57 | } 58 | 59 | if (Include is not null) 60 | { 61 | _includePatterns = [.. Include.Select(e => new WildcardPattern(e, Options))]; 62 | } 63 | } 64 | 65 | protected override void ProcessRecord() 66 | { 67 | if (InputStream is not null) 68 | { 69 | HandleFromStream(InputStream); 70 | return; 71 | } 72 | 73 | foreach (string path in EnumerateResolvedPaths()) 74 | { 75 | if (path.WriteErrorIfNotArchive( 76 | IsLiteral ? nameof(LiteralPath) : nameof(Path), this)) 77 | { 78 | continue; 79 | } 80 | 81 | try 82 | { 83 | WriteObject( 84 | GetEntriesFromFile(path).ToEntrySort(), 85 | enumerateCollection: true); 86 | } 87 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 88 | { 89 | throw; 90 | } 91 | catch (Exception exception) when (IsInvalidArchive(exception)) 92 | { 93 | ThrowTerminatingError(exception.ToInvalidArchive(ArchiveType)); 94 | } 95 | catch (Exception exception) 96 | { 97 | WriteError(exception.ToOpenError(path)); 98 | } 99 | } 100 | } 101 | 102 | private void HandleFromStream(Stream stream) 103 | { 104 | try 105 | { 106 | if (stream.CanSeek) 107 | { 108 | stream.Seek(0, SeekOrigin.Begin); 109 | } 110 | 111 | WriteObject( 112 | GetEntriesFromStream(stream).ToEntrySort(), 113 | enumerateCollection: true); 114 | } 115 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 116 | { 117 | throw; 118 | } 119 | catch (Exception exception) when (IsInvalidArchive(exception)) 120 | { 121 | ThrowTerminatingError(exception.ToInvalidArchive(ArchiveType, isStream: true)); 122 | } 123 | catch (Exception exception) 124 | { 125 | WriteError(exception.ToOpenError("InputStream")); 126 | } 127 | } 128 | 129 | protected abstract IEnumerable GetEntriesFromFile(string path); 130 | 131 | protected abstract IEnumerable GetEntriesFromStream(Stream stream); 132 | 133 | private static bool MatchAny( 134 | string name, 135 | WildcardPattern[] patterns) 136 | { 137 | foreach (WildcardPattern pattern in patterns) 138 | { 139 | if (pattern.IsMatch(name)) 140 | { 141 | return true; 142 | } 143 | } 144 | 145 | return false; 146 | } 147 | 148 | protected bool ShouldInclude(string name) 149 | { 150 | if (_includePatterns is null) 151 | { 152 | return true; 153 | } 154 | 155 | return MatchAny(name, _includePatterns); 156 | } 157 | 158 | protected bool ShouldExclude(string name) 159 | { 160 | if (_excludePatterns is null) 161 | { 162 | return false; 163 | } 164 | 165 | return MatchAny(name, _excludePatterns); 166 | } 167 | 168 | protected bool ShouldSkipEntry(bool isDirectory) => 169 | isDirectory && Type is EntryType.Archive || !isDirectory && Type is EntryType.Directory; 170 | 171 | private static bool IsInvalidArchive(Exception exception) => 172 | exception is ZipException or TarException or ZstdException or IOException; 173 | } 174 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 12/13/2025 4 | 5 | - **Native Zip Entry Objects** 6 | 7 | Zip entries returned by `Get-ZipEntry` (and created by `New-ZipEntry`) are now backed directly by `ICSharpCode.SharpZipLib.Zip.ZipEntry`. 8 | This exposes additional useful properties on `ZipEntryBase` derived objects: 9 | - `IsEncrypted` (`bool`) – Indicates whether the entry is encrypted. 10 | - `AESKeySize` (`int`) – AES key size (0, 128, 192, or 256) if AES encryption is used. 11 | - `CompressionMethod` (`ICSharpCode.SharpZipLib.Zip.CompressionMethod`) – The actual compression method used. 12 | - `Comment` (`string`) – The entry comment. 13 | - `Crc` (`long`) – Cyclic redundancy check. 14 | 15 | - **Support for Encrypted Zip Entries** 16 | 17 | `Get-ZipEntryContent` and `Expand-ZipEntry` now fully support reading and extracting password-protected entries. 18 | A new common parameter has been added to both cmdlets: 19 | 20 | ```powershell 21 | -Password 22 | ``` 23 | 24 | - If an entry is encrypted and no password is provided, the cmdlets will securely prompt for one. 25 | - Examples and detailed guidance for handling encrypted entries have been added to the help documentation. 26 | 27 | - **Documentation Improvements** 28 | 29 | All cmdlet help files have been reviewed and updated for consistency and clarity. 30 | Significant enhancements to `Get-ZipEntryContent` and `Expand-ZipEntry` help: 31 | - Added dedicated examples demonstrating password-protected entry handling. 32 | - Updated parameter descriptions and notes for the new `-Password` parameter. 33 | - Improved phrasing, removed outdated example output, and ensured uniform formatting across the module. 34 | 35 | ## 07/02/2025 36 | 37 | - Added `AssemblyLoadContext` support for PowerShell 7 (.NET 8.0 or later) to resolve DLL hell by isolating module dependencies. PowerShell 5.1 (.NET Framework) users can't get around this issue due to lack of `AssemblyLoadContext` in that runtime. 38 | 39 | ## 06/23/2025 40 | 41 | - Added commands supporting several algorithms to compress and decompress strings: 42 | - `ConvertFrom-BrotliString` & `ConvertTo-BrotliString` (using to BrotliSharpLib) 43 | - `ConvertFrom-DeflateString` & `ConvertTo-DeflateString` (from CLR) 44 | - `ConvertFrom-ZlibString` & `ConvertTo-ZlibString` (custom implementation) 45 | - Added commands for `.tar` entry management with a reduced set of operations compared to `zip` entry management: 46 | - `Get-TarEntry`: Lists entries, serving as the main entry point for `TarEntry` cmdlets. 47 | - `Get-TarEntryContent`: Retrieves the content of a tar entry. 48 | - `Expand-TarEntry`: Extracts a tar entry to a file. 49 | - Added commands to compress files and folders into `.tar` archives and extract `.tar` archives with various compression algorithms: 50 | - `Compress-TarArchive` & `Expand-TarArchive`: Supported compression algorithms include `gz`, `bz2`, `zst`, `lz`, and `none` (no compression). 51 | - Removed commands: 52 | - `Compress-GzipArchive` & `Expand-GzipArchive`: These were deprecated as they only supported single-file compression, which is now better handled by the module’s `.tar` archive functionality. For a workaround to compress or decompress single files using gzip, see [Example 2 in `ConvertTo-GzipString`][example2converttogzipstring], which demonstrates using: 53 | 54 | ```powershell 55 | [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-GzipString 56 | ``` 57 | 58 | This update was made possible by the following projects. If you find them helpful, please consider starring their repositories: 59 | 60 | - [SharpZipLib](https://github.com/icsharpcode/SharpZipLib) 61 | - [SharpCompress](https://github.com/adamhathcock/sharpcompress) 62 | - [BrotliSharpLib](https://github.com/master131/BrotliSharpLib) 63 | - [ZstdSharp](https://github.com/oleg-st/ZstdSharp) 64 | 65 | ## 01/10/2025 66 | 67 | - Code improvements. 68 | - Instance methods `.OpenRead()` and `.OpenWrite()` moved from `ZipEntryFile` to `ZipEntryBase`. 69 | - Adds support to list, read and extract zip archive entries from Stream. 70 | 71 | ## 06/24/2024 72 | 73 | - Update build process. 74 | 75 | ## 06/05/2024 76 | 77 | - Update `ci.yml` to use `codecov-action@v4`. 78 | - Fixed parameter names in `Compress-ZipArchive` documentation. Thanks to @martincostello. 79 | - Fixed coverlet.console support for Linux runner tests. 80 | 81 | ## 02/26/2024 82 | 83 | - Fixed a bug with `CompressionRatio` property showing always in InvariantCulture format. 84 | 85 | ## 02/25/2024 86 | 87 | - `ZipEntryBase` Type: 88 | - Renamed Property `EntryName` to `Name`. 89 | - Renamed Property `EntryRelativePath` to `RelativePath`. 90 | - Renamed Property `EntryType` to `Type`. 91 | - Renamed Method `RemoveEntry()` to `Remove()`. 92 | - Added Property `CompressionRatio`. 93 | - `ZipEntryFile` Type: 94 | - Added Property `Extension`. 95 | - Added Property `BaseName`. 96 | - `ZipEntryDirectory` Type: 97 | - `.Name` Property now reflects the directory entries name instead of an empty string. 98 | - Added command `Rename-ZipEntry`. 99 | - `NormalizePath` Method: 100 | - Moved from `[PSCompression.ZipEntryExtensions]::NormalizePath` to `[PSCompression.Extensions.PathExtensions]::NormalizePath`. 101 | - `Get-ZipEntry` command: 102 | - Renamed Parameter `-EntryType` to `-Type`. 103 | 104 | [example2converttogzipstring]: https://github.com/santisq/PSCompression/blob/main/docs/en-US/ConvertTo-GzipString.md#example-2-create-a-gzip-compressed-file-from-a-string 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | benchmarks/ 5 | BenchmarkDotNet.Artifacts/ 6 | tools/dotnet 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | *.zip 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # DNX 49 | project.lock.json 50 | project.fragment.lock.json 51 | artifacts/ 52 | 53 | *_i.c 54 | *_p.c 55 | *_i.h 56 | *.ilk 57 | *.meta 58 | *.obj 59 | *.pch 60 | *.pdb 61 | *.pgc 62 | *.pgd 63 | *.rsp 64 | *.sbr 65 | *.tlb 66 | *.tli 67 | *.tlh 68 | *.tmp 69 | *.tmp_proj 70 | *.log 71 | *.vspscc 72 | *.vssscc 73 | .builds 74 | *.pidb 75 | *.svclog 76 | *.scc 77 | 78 | # Chutzpah Test files 79 | _Chutzpah* 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opendb 86 | *.opensdf 87 | *.sdf 88 | *.cachefile 89 | *.VC.db 90 | *.VC.VC.opendb 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | *.sap 97 | 98 | # TFS 2012 Local Workspace 99 | $tf/ 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | *.DotSettings.user 108 | 109 | # JustCode is a .NET coding add-in 110 | .JustCode 111 | 112 | # TeamCity is a build add-in 113 | _TeamCity* 114 | 115 | # DotCover is a Code Coverage Tool 116 | *.dotCover 117 | 118 | # NCrunch 119 | _NCrunch_* 120 | .*crunch*.local.xml 121 | nCrunchTemp_* 122 | 123 | # MightyMoose 124 | *.mm.* 125 | AutoTest.Net/ 126 | 127 | # Web workbench (sass) 128 | .sass-cache/ 129 | 130 | # Installshield output folder 131 | [Ee]xpress/ 132 | 133 | # DocProject is a documentation generator add-in 134 | DocProject/buildhelp/ 135 | DocProject/Help/*.HxT 136 | DocProject/Help/*.HxC 137 | DocProject/Help/*.hhc 138 | DocProject/Help/*.hhk 139 | DocProject/Help/*.hhp 140 | DocProject/Help/Html2 141 | DocProject/Help/html 142 | 143 | # Click-Once directory 144 | publish/ 145 | 146 | # Publish Web Output 147 | *.[Pp]ublish.xml 148 | *.azurePubxml 149 | # TODO: Comment the next line if you want to checkin your web deploy settings 150 | # but database connection strings (with potential passwords) will be unencrypted 151 | #*.pubxml 152 | *.publishproj 153 | 154 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 155 | # checkin your Azure Web App publish settings, but sensitive information contained 156 | # in these scripts will be unencrypted 157 | PublishScripts/ 158 | 159 | # NuGet Packages 160 | *.nupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | # NuGet v3's project.json files produces more ignoreable files 168 | *.nuget.props 169 | *.nuget.targets 170 | 171 | # Microsoft Azure Build Output 172 | csx/ 173 | *.build.csdef 174 | 175 | # Microsoft Azure Emulator 176 | ecf/ 177 | rcf/ 178 | 179 | # Windows Store app package directories and files 180 | AppPackages/ 181 | BundleArtifacts/ 182 | Package.StoreAssociation.xml 183 | _pkginfo.txt 184 | 185 | # Visual Studio cache files 186 | # files ending in .cache can be ignored 187 | *.[Cc]ache 188 | # but keep track of directories ending in .cache 189 | !*.[Cc]ache/ 190 | 191 | # Others 192 | ClientBin/ 193 | ~$* 194 | *~ 195 | *.dbmdl 196 | *.dbproj.schemaview 197 | *.jfm 198 | *.pfx 199 | *.publishsettings 200 | node_modules/ 201 | orleans.codegen.cs 202 | 203 | # Since there are multiple workflows, uncomment next line to ignore bower_components 204 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 205 | #bower_components/ 206 | 207 | # RIA/Silverlight projects 208 | Generated_Code/ 209 | 210 | # Backup & report files from converting an old project file 211 | # to a newer Visual Studio version. Backup files are not needed, 212 | # because we have git ;-) 213 | _UpgradeReport_Files/ 214 | Backup*/ 215 | UpgradeLog*.XML 216 | UpgradeLog*.htm 217 | 218 | # SQL Server files 219 | *.mdf 220 | *.ldf 221 | 222 | # Business Intelligence projects 223 | *.rdl.data 224 | *.bim.layout 225 | *.bim_*.settings 226 | 227 | # Microsoft Fakes 228 | FakesAssemblies/ 229 | 230 | # GhostDoc plugin setting file 231 | *.GhostDoc.xml 232 | 233 | # Node.js Tools for Visual Studio 234 | .ntvs_analysis.dat 235 | 236 | # Visual Studio 6 build log 237 | *.plg 238 | 239 | # Visual Studio 6 workspace options file 240 | *.opt 241 | 242 | # Visual Studio LightSwitch build output 243 | **/*.HTMLClient/GeneratedArtifacts 244 | **/*.DesktopClient/GeneratedArtifacts 245 | **/*.DesktopClient/ModelManifest.xml 246 | **/*.Server/GeneratedArtifacts 247 | **/*.Server/ModelManifest.xml 248 | _Pvt_Extensions 249 | 250 | # Paket dependency manager 251 | .paket/paket.exe 252 | paket-files/ 253 | 254 | # FAKE - F# Make 255 | .fake/ 256 | 257 | # JetBrains Rider 258 | .idea/ 259 | *.sln.iml 260 | 261 | # CodeRush 262 | .cr/ 263 | 264 | # Python Tools for Visual Studio (PTVS) 265 | __pycache__/ 266 | *.pyc 267 | 268 | ### Custom entries ### 269 | output/ 270 | tools/Modules 271 | test.settings.json 272 | tests/integration/.vagrant 273 | tests/integration/cert_setup 274 | !assets/*.zip 275 | -------------------------------------------------------------------------------- /src/PSCompression/Exceptions/ExceptionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Management.Automation; 6 | using ICSharpCode.SharpZipLib.Zip; 7 | using PSCompression.Abstractions; 8 | using PSCompression.Extensions; 9 | 10 | namespace PSCompression.Exceptions; 11 | 12 | internal static class ExceptionHelper 13 | { 14 | private static readonly char[] s_InvalidFileNameChar = Path.GetInvalidFileNameChars(); 15 | 16 | private static readonly char[] s_InvalidPathChar = Path.GetInvalidPathChars(); 17 | 18 | internal static bool WriteErrorIfNotArchive( 19 | this string path, 20 | string paramname, 21 | PSCmdlet cmdlet, 22 | bool isTerminating = false) 23 | { 24 | if (File.Exists(path)) 25 | { 26 | return false; 27 | } 28 | 29 | ArgumentException exception = new( 30 | $"The specified path '{path}' does not exist or is a Directory.", 31 | paramname); 32 | 33 | ErrorRecord error = new(exception, "NotArchivePath", ErrorCategory.InvalidArgument, path); 34 | 35 | if (isTerminating) 36 | { 37 | cmdlet.ThrowTerminatingError(error); 38 | } 39 | 40 | cmdlet.WriteError(error); 41 | return true; 42 | } 43 | 44 | 45 | internal static ErrorRecord NotDirectoryPath(string path, string paramname) => 46 | new( 47 | new ArgumentException( 48 | $"Destination path is an existing File: '{path}'.", paramname), 49 | "NotDirectoryPath", ErrorCategory.InvalidArgument, path); 50 | 51 | internal static ErrorRecord ToInvalidProviderError(this ProviderInfo provider, string path) => 52 | new( 53 | new NotSupportedException( 54 | $"The resolved path '{path}' is not a FileSystem path but '{provider.Name}'."), 55 | "NotFileSystemPath", ErrorCategory.InvalidArgument, path); 56 | 57 | internal static ErrorRecord ToOpenError(this Exception exception, string path) => 58 | new(exception, "EntryOpen", ErrorCategory.OpenError, path); 59 | 60 | internal static ErrorRecord ToResolvePathError(this Exception exception, string path) => 61 | new(exception, "ResolvePath", ErrorCategory.NotSpecified, path); 62 | 63 | internal static ErrorRecord ToExtractEntryError(this Exception exception, object entry) => 64 | new(exception, "ExtractEntry", ErrorCategory.NotSpecified, entry); 65 | 66 | internal static ErrorRecord ToStreamOpenError(this Exception exception, object item) => 67 | new(exception, "StreamOpen", ErrorCategory.NotSpecified, item); 68 | 69 | internal static ErrorRecord ToWriteError(this Exception exception, object? item) => 70 | new(exception, "WriteError", ErrorCategory.WriteError, item); 71 | 72 | internal static ErrorRecord ToDuplicatedEntryError(this DuplicatedEntryException exception) => 73 | new(exception, "DuplicatedEntry", ErrorCategory.WriteError, exception._path); 74 | 75 | internal static ErrorRecord ToInvalidNameError(this InvalidNameException exception, string name) => 76 | new(exception, "InvalidName", ErrorCategory.InvalidArgument, name); 77 | 78 | internal static ErrorRecord ToEntryNotFoundError(this EntryNotFoundException exception) => 79 | new(exception, "EntryNotFound", ErrorCategory.ObjectNotFound, exception._path); 80 | 81 | internal static ErrorRecord ToEnumerationError(this Exception exception, object item) => 82 | new(exception, "EnumerationError", ErrorCategory.ReadError, item); 83 | 84 | internal static ErrorRecord ToInvalidArchive( 85 | this Exception exception, 86 | ArchiveType type, 87 | bool isStream = false) 88 | { 89 | string basemsg = $"Specified path or stream is not a valid {type} archive, " + 90 | "might be compressed using an unsupported method, " + 91 | "or could be corrupted."; 92 | 93 | if (type == ArchiveType.tar && isStream) 94 | { 95 | basemsg += " When reading a tar archive from a stream, " + 96 | "use the -Algorithm parameter to specify the compression type."; 97 | } 98 | 99 | return new ErrorRecord( 100 | new InvalidDataException(basemsg, exception), 101 | "InvalidArchive", ErrorCategory.InvalidData, null); 102 | } 103 | 104 | 105 | internal static void ThrowIfNotFound( 106 | this ZipArchive zip, 107 | string path, 108 | string source, 109 | [NotNull] out ZipArchiveEntry? entry) 110 | { 111 | if (!zip.TryGetEntry(path, out entry)) 112 | { 113 | throw EntryNotFoundException.Create(path, source); 114 | } 115 | } 116 | 117 | internal static void ThrowIfNotFound( 118 | this ICSharpCode.SharpZipLib.Zip.ZipFile zip, 119 | string path, 120 | string source, 121 | [NotNull] out ZipEntry? entry) 122 | { 123 | if (!zip.TryGetEntry(path, out entry)) 124 | { 125 | throw EntryNotFoundException.Create(path, source); 126 | } 127 | } 128 | 129 | internal static void ThrowIfDuplicate( 130 | this ZipArchive zip, 131 | string path, 132 | string source) 133 | { 134 | if (zip.TryGetEntry(path, out ZipArchiveEntry? _)) 135 | { 136 | throw DuplicatedEntryException.Create(path, source); 137 | } 138 | } 139 | 140 | internal static void ThrowIfInvalidNameChar(this string newname) 141 | { 142 | if (newname.IndexOfAny(s_InvalidFileNameChar) != -1) 143 | { 144 | throw InvalidNameException.Create(newname); 145 | } 146 | } 147 | 148 | internal static void ThrowIfInvalidPathChar(this string path) 149 | { 150 | if (path.IndexOfAny(s_InvalidPathChar) != -1) 151 | { 152 | throw new ArgumentException( 153 | $"Path: '{path}' contains invalid path characters."); 154 | } 155 | } 156 | 157 | internal static void ThrowIfFromStream(this ZipEntryBase entry) 158 | { 159 | if (entry.FromStream) 160 | { 161 | throw new NotSupportedException( 162 | "The operation is not supported for entries created from input Stream."); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/ProjectInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Management.Automation; 7 | using System.Xml; 8 | 9 | namespace ProjectBuilder; 10 | 11 | public sealed class ProjectInfo 12 | { 13 | public DirectoryInfo Root { get; } 14 | 15 | public Module Module { get; } 16 | 17 | public Configuration Configuration { get; internal set; } 18 | 19 | public Documentation Documentation { get; internal set; } 20 | 21 | public Project Project { get; } 22 | 23 | public Pester Pester { get; } 24 | 25 | public Version PowerShellVersion { get; } 26 | 27 | public string? AnalyzerPath 28 | { 29 | get 30 | { 31 | _analyzerPath ??= Path.Combine( 32 | Root.FullName, 33 | "ScriptAnalyzerSettings.psd1"); 34 | 35 | if (File.Exists(_analyzerPath)) 36 | { 37 | return _analyzerPath; 38 | } 39 | 40 | return null; 41 | } 42 | } 43 | 44 | private string? _analyzerPath; 45 | 46 | private ProjectInfo(string path, Version psVersion) 47 | { 48 | PowerShellVersion = psVersion; 49 | Root = AssertDirectory(path); 50 | 51 | Module = new Module( 52 | directory: AssertDirectory(GetModulePath(path)), 53 | name: Path.GetFileNameWithoutExtension(path), 54 | info: this); 55 | 56 | Project = new Project( 57 | source: AssertDirectory(GetSourcePath(path, Module.Name)), 58 | build: GetBuildPath(path), 59 | info: this); 60 | 61 | Pester = new(this); 62 | } 63 | 64 | public static ProjectInfo Create( 65 | string path, 66 | Configuration configuration, 67 | Version psVersion) 68 | { 69 | ProjectInfo builder = new(path, psVersion) 70 | { 71 | Configuration = configuration 72 | }; 73 | 74 | builder.Module.Manifest = GetManifest(builder); 75 | builder.Module.Version = GetManifestVersion(builder); 76 | builder.Project.Release = GetReleasePath( 77 | builder.Project.Build, 78 | builder.Module.Name, 79 | builder.Module.Version!); 80 | builder.Project.TargetFrameworks = GetTargetFrameworks(GetProjectFile(builder)); 81 | builder.Documentation = new Documentation 82 | { 83 | Source = Path.Combine(builder.Root.FullName, "docs", "en-US"), 84 | Output = Path.Combine(builder.Project.Release, "en-US") 85 | }; 86 | 87 | return builder; 88 | } 89 | 90 | public IEnumerable GetRequirements() 91 | { 92 | string req = Path.Combine(Root.FullName, "tools", "requiredModules.psd1"); 93 | if (!File.Exists(req)) 94 | { 95 | return []; 96 | } 97 | return Module.GetRequirements(req); 98 | } 99 | 100 | public void CleanRelease() 101 | { 102 | if (Directory.Exists(Project.Release)) 103 | { 104 | Directory.Delete(Project.Release, recursive: true); 105 | } 106 | Directory.CreateDirectory(Project.Release); 107 | } 108 | 109 | public string[] GetBuildArgs() => 110 | [ 111 | "publish", 112 | "--configuration", Configuration.ToString(), 113 | "--verbosity", "q", 114 | "-nologo", 115 | $"-p:Version={Module.Version}" 116 | ]; 117 | 118 | public Hashtable GetAnalyzerParams() => new() 119 | { 120 | ["Path"] = Project.Release, 121 | ["Settings"] = AnalyzerPath, 122 | ["Recurse"] = true, 123 | ["ErrorAction"] = "SilentlyContinue" 124 | }; 125 | 126 | private static string[] GetTargetFrameworks(string path) 127 | { 128 | XmlDocument xmlDocument = new(); 129 | xmlDocument.Load(path); 130 | return xmlDocument 131 | .SelectSingleNode("Project/PropertyGroup/TargetFrameworks") 132 | .InnerText 133 | .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); 134 | } 135 | 136 | private static string GetBuildPath(string path) => 137 | Path.Combine(path, "output"); 138 | 139 | private static string GetSourcePath(string path, string moduleName) => 140 | Path.Combine(path, "src", moduleName); 141 | 142 | private static string GetModulePath(string path) => 143 | Path.Combine(path, "module"); 144 | 145 | private static string GetReleasePath( 146 | string buildPath, 147 | string moduleName, 148 | Version version) => Path.Combine( 149 | buildPath, 150 | moduleName, 151 | LanguagePrimitives.ConvertTo(version)); 152 | 153 | private static DirectoryInfo AssertDirectory(string path) 154 | { 155 | DirectoryInfo directory = new(path); 156 | return directory.Exists ? directory 157 | : throw new ArgumentException( 158 | $"Path '{path}' could not be found or is not a Directory.", 159 | nameof(path)); 160 | } 161 | 162 | private static FileInfo GetManifest(ProjectInfo builder) => 163 | builder.Module.Root.EnumerateFiles("*.psd1").FirstOrDefault() 164 | ?? throw new FileNotFoundException( 165 | $"Manifest file could not be found in '{builder.Root.FullName}'"); 166 | 167 | private static string GetProjectFile(ProjectInfo builder) => 168 | builder.Project.Source.EnumerateFiles("*.csproj").FirstOrDefault()?.FullName 169 | ?? throw new FileNotFoundException( 170 | $"Project file could not be found in ''{builder.Project.Source.FullName}'"); 171 | 172 | private static Version? GetManifestVersion(ProjectInfo builder) 173 | { 174 | using PowerShell powershell = PowerShell.Create(RunspaceMode.CurrentRunspace); 175 | PSModuleInfo? moduleInfo = powershell 176 | .AddCommand("Test-ModuleManifest") 177 | .AddParameters(new Dictionary() 178 | { 179 | ["Path"] = builder.Module.Manifest?.FullName, 180 | ["ErrorAction"] = "Ignore" 181 | }) 182 | .Invoke() 183 | .FirstOrDefault(); 184 | 185 | return powershell.Streams.Error is { Count: > 0 } 186 | ? throw powershell.Streams.Error.First().Exception 187 | : moduleInfo.Version; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /docs/en-US/Expand-TarEntry.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # Expand-TarEntry 9 | 10 | ## SYNOPSIS 11 | 12 | Extracts selected tar archive entries to a destination directory while preserving their relative paths. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | Expand-TarEntry 18 | -InputObject 19 | [[-Destination] ] 20 | [-Force] 21 | [-PassThru] 22 | [] 23 | ``` 24 | 25 | ## DESCRIPTION 26 | 27 | The `Expand-TarEntry` cmdlet extracts tar entries produced by [`Get-TarEntry`](./Get-TarEntry.md) to a destination directory. Extracted entries preserve their original relative paths and directory structure from the archive. It works with both uncompressed and compressed tar archives that have been processed by `Get-TarEntry`. 28 | 29 | ## EXAMPLES 30 | 31 | ### Example 1: Extract all `.txt` files from a tar archive to the current directory 32 | 33 | ```powershell 34 | PS C:\> Get-TarEntry .\archive.tar -Include *.txt | Expand-TarEntry 35 | ``` 36 | 37 | This example extracts only the `.txt` files from `archive.tar` to the current directory, preserving their relative paths within the archive. 38 | 39 | ### Example 2: Extract all `.txt` files from a tar archive to a specific directory 40 | 41 | ```powershell 42 | PS C:\> Get-TarEntry .\archive.tar.gz -Include *.txt | Expand-TarEntry -Destination .\extracted 43 | ``` 44 | 45 | This example extracts only the `.txt` files from a gzip-compressed tar archive to the specified `.\extracted` directory (created automatically if needed). 46 | 47 | ### Example 3: Extract all entries excluding `.txt` files from a tar archive 48 | 49 | ```powershell 50 | PS C:\> Get-TarEntry .\archive.tar -Exclude *.txt | Expand-TarEntry 51 | ``` 52 | 53 | This example extracts everything except `.txt` files from `archive.tar` to the current directory, preserving the original structure. 54 | 55 | ### Example 4: Extract entries overwriting existing files 56 | 57 | ```powershell 58 | PS C:\> Get-TarEntry .\archive.tar -Include *.txt | Expand-TarEntry -Force 59 | ``` 60 | 61 | This example extracts the `.txt` files and overwrites any existing files with the same name in the destination due to the `-Force` switch. 62 | 63 | ### Example 5: Extract entries and output the expanded items 64 | 65 | ```powershell 66 | PS C:\> Get-TarEntry .\archive.tar -Exclude *.txt | Expand-TarEntry -PassThru 67 | 68 | Directory: C:\ 69 | 70 | Mode LastWriteTime Length Name 71 | ---- ------------- ------ ---- 72 | d---- 2025-06-23 7:00 PM folder1 73 | -a--- 2025-06-23 7:00 PM 2048 image.png 74 | ``` 75 | 76 | This example extracts everything except `.txt` files and uses `-PassThru` to output `FileInfo` and `DirectoryInfo` objects for the extracted items. By default, the cmdlet produces no output. 77 | 78 | ### Example 6: Extract a specific entry from a compressed tar archive 79 | 80 | ```powershell 81 | PS C:\> $stream = Invoke-WebRequest https://example.com/archive.tar.gz 82 | PS C:\> $stream | Get-TarEntry -Include readme.md -Algorithm gz | Expand-TarEntry -PassThru | Get-Content 83 | ``` 84 | 85 | This example extracts only the `readme.md` file from a gzip-compressed tar archive streamed from the web and immediately displays its contents. 86 | 87 | > [!NOTE] 88 | > When `Get-TarEntry` processes a stream, it defaults to the `gz` (gzip) algorithm. Specify `-Algorithm` on `Get-TarEntry` if the stream uses a different compression format. 89 | 90 | ## PARAMETERS 91 | 92 | ### -Destination 93 | 94 | The destination directory where tar entries are extracted. If not specified, entries are extracted to their relative path in the current directory, creating any necessary subdirectories. 95 | 96 | ```yaml 97 | Type: String 98 | Parameter Sets: (All) 99 | Aliases: 100 | 101 | Required: False 102 | Position: 0 103 | Default value: $PWD 104 | Accept pipeline input: False 105 | Accept wildcard characters: False 106 | ``` 107 | 108 | ### -Force 109 | 110 | Overwrites existing files in the destination directory. Without `-Force`, existing files are skipped. 111 | 112 | ```yaml 113 | Type: SwitchParameter 114 | Parameter Sets: (All) 115 | Aliases: 116 | 117 | Required: False 118 | Position: Named 119 | Default value: None 120 | Accept pipeline input: False 121 | Accept wildcard characters: False 122 | ``` 123 | 124 | ### -InputObject 125 | 126 | The tar entries to extract. These are instances of `TarEntryBase` (`TarEntryFile` or `TarEntryDirectory`) output by the [`Get-TarEntry`](./Get-TarEntry.md) cmdlet. 127 | 128 | > [!NOTE] 129 | > This parameter accepts pipeline input from `Get-TarEntry`. Binding by name is also supported. 130 | 131 | ```yaml 132 | Type: TarEntryBase[] 133 | Parameter Sets: (All) 134 | Aliases: 135 | 136 | Required: True 137 | Position: Named 138 | Default value: None 139 | Accept pipeline input: True (ByValue) 140 | Accept wildcard characters: False 141 | ``` 142 | 143 | ### -PassThru 144 | 145 | Outputs `System.IO.FileInfo` and `System.IO.DirectoryInfo` objects for the extracted entries. By default, the cmdlet produces no output. 146 | 147 | ```yaml 148 | Type: SwitchParameter 149 | Parameter Sets: (All) 150 | Aliases: 151 | 152 | Required: False 153 | Position: Named 154 | Default value: None 155 | Accept pipeline input: False 156 | Accept wildcard characters: False 157 | ``` 158 | 159 | ### CommonParameters 160 | 161 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 162 | 163 | ## INPUTS 164 | 165 | ### PSCompression.Abstractions.TarEntryBase[] 166 | 167 | You can pipe instances of `TarEntryFile` or `TarEntryDirectory` from [`Get-TarEntry`](./Get-TarEntry.md) to this cmdlet. 168 | 169 | ## OUTPUTS 170 | 171 | ### None 172 | 173 | By default, this cmdlet produces no output. 174 | 175 | ### System.IO.FileSystemInfo 176 | 177 | When the `-PassThru` switch is used, the cmdlet outputs `FileInfo` and `DirectoryInfo` objects representing the extracted items. 178 | 179 | ## NOTES 180 | 181 | ## RELATED LINKS 182 | 183 | [__`Get-TarEntry`__](./Get-TarEntry.md) 184 | 185 | [__`Expand-TarArchive`__](./Expand-TarArchive.md) 186 | 187 | [__SharpZipLib__](https://github.com/icsharpcode/SharpZipLib) 188 | 189 | [__SharpCompress__](https://github.com/adamhathcock/sharpcompress) 190 | 191 | [__ZstdSharp__](https://github.com/oleg-st/ZstdSharp) 192 | 193 | [__System.IO.Compression__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression) 194 | -------------------------------------------------------------------------------- /docs/en-US/ConvertTo-BrotliString.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # ConvertTo-BrotliString 9 | 10 | ## SYNOPSIS 11 | 12 | Compresses input strings into a Brotli-compressed Base64-encoded string. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | ConvertTo-BrotliString 18 | [-InputObject] 19 | [-Encoding ] 20 | [-CompressionLevel ] 21 | [-AsByteStream] 22 | [-NoNewLine] 23 | [] 24 | ``` 25 | 26 | ## DESCRIPTION 27 | 28 | The `ConvertTo-BrotliString` cmdlet compresses input strings into Brotli-compressed Base64-encoded strings or raw bytes using the `BrotliStream` class from the `BrotliSharpLib` library. It is the counterpart to [`ConvertFrom-BrotliString`](./ConvertFrom-BrotliString.md). 29 | 30 | ## EXAMPLES 31 | 32 | ### Example 1: Compress strings into a Brotli-compressed Base64 string 33 | 34 | ```powershell 35 | PS ..\pwsh> $strings = 'hello', 'world', '!' 36 | PS ..\pwsh> ConvertTo-BrotliString $strings 37 | 38 | CwiAaGVsbG8NCndvcmxkDQohDQoD 39 | 40 | # Or using pipeline input 41 | PS ..\pwsh> $strings | ConvertTo-BrotliString 42 | 43 | CwiAaGVsbG8NCndvcmxkDQohDQoD 44 | ``` 45 | 46 | This example shows how to compress an array of strings into a single Brotli-compressed Base64 string, using either argument or pipeline input. 47 | 48 | ### Example 2: Save Brotli-compressed bytes to a file using `-AsByteStream` 49 | 50 | ```powershell 51 | PS ..\pwsh> 'hello world!' | ConvertTo-BrotliString -AsByteStream | Set-Content -FilePath .\helloworld.br -AsByteStream 52 | 53 | # To read the file back you can use `ConvertFrom-BrotliString` following these steps: 54 | PS ..\pwsh> $path = Convert-Path .\helloworld.br 55 | PS ..\pwsh> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-BrotliString 56 | 57 | hello world! 58 | ``` 59 | 60 | This example shows how to use `-AsByteStream` to output raw compressed bytes that can be written to a file using `Set-Content` or `Out-File`. Note that the byte array is not enumerated. 61 | 62 | > [!NOTE] 63 | > The example uses `-AsByteStream` with `Set-Content`, which is available in PowerShell 7+. In Windows PowerShell 5.1, use `-Encoding Byte` with `Set-Content` or `Out-File` to write the byte array to a file. 64 | 65 | ### Example 3: Compress strings using a specific Encoding 66 | 67 | ```powershell 68 | PS ..\pwsh> 'ñ' | ConvertTo-BrotliString -Encoding ansi | ConvertFrom-BrotliString 69 | � 70 | 71 | PS ..\pwsh> 'ñ' | ConvertTo-BrotliString -Encoding utf8BOM | ConvertFrom-BrotliString 72 | ñ 73 | ``` 74 | 75 | This example shows how different encodings affect the compression and decompression of special characters. The default encoding is `utf8NoBOM`. 76 | 77 | ### Example 4: Compress the contents of multiple files into a single Brotli Base64 string 78 | 79 | ```powershell 80 | # Check the total length of the files 81 | PS ..\pwsh> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb 82 | 87.216796875 83 | 84 | # Check the total length after compression 85 | PS ..\pwsh> (Get-Content myLogs\*.txt | ConvertTo-BrotliString).Length / 1kb 86 | 35.123456789 87 | ``` 88 | 89 | This example demonstrates compressing the contents of multiple text files into a single Brotli-compressed Base64 string and compares the total length before and after compression. 90 | 91 | ## PARAMETERS 92 | 93 | ### -AsByteStream 94 | 95 | Outputs the compressed byte array to the Success Stream. 96 | 97 | > [!NOTE] 98 | > This parameter is intended for use with cmdlets that accept byte arrays, such as `Out-File` and `Set-Content` with `-Encoding Byte` (Windows PowerShell 5.1) or `-AsByteStream` (PowerShell 7+). 99 | 100 | ```yaml 101 | Type: SwitchParameter 102 | Parameter Sets: (All) 103 | Aliases: Raw 104 | 105 | Required: False 106 | Position: Named 107 | Default value: False 108 | Accept pipeline input: False 109 | Accept wildcard characters: False 110 | ``` 111 | 112 | ### -CompressionLevel 113 | 114 | Specifies the compression level for the Brotli algorithm, balancing speed and compression size. See [`CompressionLevel` Enum](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.compressionlevel) for details. 115 | 116 | ```yaml 117 | Type: CompressionLevel 118 | Parameter Sets: (All) 119 | Aliases: 120 | Accepted values: Optimal, Fastest, NoCompression, SmallestSize 121 | 122 | Required: False 123 | Position: Named 124 | Default value: Optimal 125 | Accept pipeline input: False 126 | Accept wildcard characters: False 127 | ``` 128 | 129 | ### -Encoding 130 | 131 | Determines the character encoding used when compressing the input strings. 132 | 133 | > [!NOTE] 134 | > The default encoding is UTF-8 without BOM. 135 | 136 | ```yaml 137 | Type: Encoding 138 | Parameter Sets: (All) 139 | Aliases: 140 | 141 | Required: False 142 | Position: Named 143 | Default value: utf8NoBOM 144 | Accept pipeline input: False 145 | Accept wildcard characters: False 146 | ``` 147 | 148 | ### -InputObject 149 | 150 | Specifies the input string or strings to compress. 151 | 152 | ```yaml 153 | Type: String[] 154 | Parameter Sets: (All) 155 | Aliases: 156 | 157 | Required: True 158 | Position: 0 159 | Default value: None 160 | Accept pipeline input: True (ByValue) 161 | Accept wildcard characters: False 162 | ``` 163 | 164 | ### -NoNewLine 165 | 166 | The encoded string representation of the input objects is concatenated to form the output. No newline character is added after each input string when this switch is used. 167 | 168 | ```yaml 169 | Type: SwitchParameter 170 | Parameter Sets: (All) 171 | Aliases: 172 | 173 | Required: False 174 | Position: Named 175 | Default value: False 176 | Accept pipeline input: False 177 | Accept wildcard characters: False 178 | ``` 179 | 180 | ### CommonParameters 181 | 182 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 183 | 184 | ## INPUTS 185 | 186 | ### System.String[] 187 | 188 | You can pipe one or more strings to this cmdlet. 189 | 190 | ## OUTPUTS 191 | 192 | ### System.String 193 | 194 | By default, this cmdlet outputs a single Base64-encoded string. 195 | 196 | ### System.Byte[] 197 | 198 | When the `-AsByteStream` switch is used, this cmdlet outputs a byte array down the pipeline. 199 | 200 | ## NOTES 201 | 202 | ## RELATED LINKS 203 | 204 | [__`ConvertFrom-BrotliString`__](./ConvertFrom-BrotliString.md) 205 | 206 | [__BrotliSharpLib__](https://github.com/master131/BrotliSharpLib) 207 | 208 | [__System.IO.Compression__](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression) 209 | --------------------------------------------------------------------------------