├── 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 |
--------------------------------------------------------------------------------