From d2deca3660e3192b118b10874ae4ecbbdc0fa7f2 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 8 Jan 2025 18:56:49 -0300 Subject: [PATCH 01/15] lots of tidying up and organizing --- docs/en-US/Expand-ZipEntry.md | 5 +- src/PSCompression/CommandWithPathBase.cs | 86 +++++++++ .../Commands/CompressGzipArchiveCommand.cs | 73 ++----- .../Commands/CompressZipArchiveCommand.cs | 84 ++------- .../Commands/ExpandGzipArchiveCommand.cs | 58 +++--- .../Commands/ExpandZipEntryCommand.cs | 33 ++-- .../Commands/GetZipEntryCommand.cs | 102 +++++----- .../Commands/GetZipEntryContentCommand.cs | 20 +- .../Commands/NewZipEntryCommand.cs | 45 ++--- .../Commands/RemoveZipEntryCommand.cs | 6 +- .../Commands/RenameZipEntryCommand.cs | 6 +- .../Commands/SetZipEntryContentCommand.cs | 12 +- .../Exceptions/ExceptionHelpers.cs | 19 +- .../Extensions/PathExtensions.cs | 178 +++++++++++------- src/PSCompression/ZipArchiveCache.cs | 4 +- src/PSCompression/ZipContentOpsBase.cs | 6 +- src/PSCompression/ZipContentReader.cs | 35 ++-- src/PSCompression/ZipContentWriter.cs | 4 +- src/PSCompression/ZipEntryBase.cs | 6 +- src/PSCompression/ZipEntryFile.cs | 2 + src/PSCompression/internal/_Format.cs | 2 - 21 files changed, 400 insertions(+), 386 deletions(-) create mode 100644 src/PSCompression/CommandWithPathBase.cs diff --git a/docs/en-US/Expand-ZipEntry.md b/docs/en-US/Expand-ZipEntry.md index 1353ce9..520678e 100644 --- a/docs/en-US/Expand-ZipEntry.md +++ b/docs/en-US/Expand-ZipEntry.md @@ -66,7 +66,10 @@ By default this cmdlet produces no output. When `-PassThru` is used, this cmdlet ### -Destination -The destination directory where to extract the Zip Entries. This parameter is optional, when not used, the entries are extracted to the their relative zip path in the current directory. +The destination directory where to extract the Zip Entries. + +> [!NOTE] +> This parameter is optional, when not used, the entries are extracted to the their relative zip path in the current directory. ```yaml Type: String diff --git a/src/PSCompression/CommandWithPathBase.cs b/src/PSCompression/CommandWithPathBase.cs new file mode 100644 index 0000000..f0fa283 --- /dev/null +++ b/src/PSCompression/CommandWithPathBase.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Management.Automation; +using PSCompression.Exceptions; +using PSCompression.Extensions; + +namespace PSCompression; + +[EditorBrowsable(EditorBrowsableState.Never)] +public abstract class CommandWithPathBase : PSCmdlet +{ + protected string[] _paths = []; + + protected bool IsLiteral { get => LiteralPath is not null; } + + [Parameter( + ParameterSetName = "Path", + Position = 0, + Mandatory = true, + ValueFromPipeline = true)] + [SupportsWildcards] + public virtual string[] Path + { + get => _paths; + set => _paths = value; + } + + [Parameter( + ParameterSetName = "LiteralPath", + Mandatory = true, + ValueFromPipelineByPropertyName = true)] + [Alias("PSPath")] + public virtual string[] LiteralPath + { + get => _paths; + set => _paths = value; + } + + protected IEnumerable EnumerateResolvedPaths( + bool throwOnInvalidProvider = false) + { + Collection resolvedPaths; + ProviderInfo provider; + + foreach (string path in _paths) + { + if (IsLiteral) + { + string resolved = SessionState.Path.GetUnresolvedProviderPathFromPSPath( + path: path, + provider: out provider, + drive: out _); + + if (provider.Validate(path, throwOnInvalidProvider, this)) + { + yield return resolved; + } + + continue; + } + + try + { + resolvedPaths = GetResolvedProviderPathFromPSPath(path, out provider); + } + catch (Exception exception) + { + WriteError(exception.ToResolvePathError(path)); + continue; + } + + + foreach (string resolvedPath in resolvedPaths) + { + if (!provider.Validate(path, throwOnInvalidProvider, this)) + { + yield return resolvedPath; + } + } + } + } + + protected string ResolvePath(string path) => path.ResolvePath(this); +} diff --git a/src/PSCompression/Commands/CompressGzipArchiveCommand.cs b/src/PSCompression/Commands/CompressGzipArchiveCommand.cs index 405bea3..ab42ddf 100644 --- a/src/PSCompression/Commands/CompressGzipArchiveCommand.cs +++ b/src/PSCompression/Commands/CompressGzipArchiveCommand.cs @@ -10,45 +10,20 @@ namespace PSCompression.Commands; [Cmdlet(VerbsData.Compress, "GzipArchive", DefaultParameterSetName = "Path")] [OutputType(typeof(FileInfo))] [Alias("gziptofile")] -public sealed class CompressGzipArchive : PSCmdlet, IDisposable +public sealed class CompressGzipArchive : CommandWithPathBase, IDisposable { - private bool _isLiteral; - - private string[] _paths = []; - private FileStream? _destination; private GZipStream? _gzip; - [Parameter( - ParameterSetName = "Path", - Mandatory = true, - Position = 0, - ValueFromPipeline = true)] - [SupportsWildcards] - public string[] Path + private FileMode Mode { - get => _paths; - set + get => (Update.IsPresent, Force.IsPresent) switch { - _paths = value; - _isLiteral = false; - } - } - - [Parameter( - ParameterSetName = "LiteralPath", - Mandatory = true, - ValueFromPipelineByPropertyName = true)] - [Alias("PSPath")] - public string[] LiteralPath - { - get => _paths; - set - { - _paths = value; - _isLiteral = true; - } + (true, _) => FileMode.Append, + (_, true) => FileMode.Create, + _ => FileMode.CreateNew + }; } [Parameter( @@ -75,14 +50,10 @@ public string[] LiteralPath protected override void BeginProcessing() { - if (!HasGZipExtension(Destination)) - { - Destination += ".gz"; - } + Destination = ResolvePath(Destination).AddExtensionIfMissing(".gz"); try { - Destination = Destination.NormalizePath(isLiteral: true, this); string parent = Destination.GetParent(); if (!Directory.Exists(parent)) @@ -90,7 +61,7 @@ protected override void BeginProcessing() Directory.CreateDirectory(parent); } - _destination = File.Open(Destination, GetMode()); + _destination = File.Open(Destination, Mode); } catch (Exception exception) { @@ -118,13 +89,13 @@ protected override void ProcessRecord() _gzip ??= new GZipStream(_destination, CompressionLevel); - foreach (string path in _paths.NormalizePath(_isLiteral, this)) + foreach (string path in EnumerateResolvedPaths()) { if (!path.IsArchive()) { - WriteError(ExceptionHelpers.NotArchivePathError( + WriteError(ExceptionHelper.NotArchivePath( path, - _isLiteral ? nameof(LiteralPath) : nameof(Path))); + IsLiteral ? nameof(LiteralPath) : nameof(Path))); continue; } @@ -152,28 +123,10 @@ protected override void EndProcessing() } } - private FileMode GetMode() - { - if (Update.IsPresent) - { - return FileMode.Append; - } - - if (Force.IsPresent) - { - return FileMode.Create; - } - - return FileMode.CreateNew; - } - - private bool HasGZipExtension(string path) => - System.IO.Path.GetExtension(path) - .Equals(".gz", StringComparison.InvariantCultureIgnoreCase); - public void Dispose() { _gzip?.Dispose(); _destination?.Dispose(); + GC.SuppressFinalize(this); } } diff --git a/src/PSCompression/Commands/CompressZipArchiveCommand.cs b/src/PSCompression/Commands/CompressZipArchiveCommand.cs index f5faa9c..8e20254 100644 --- a/src/PSCompression/Commands/CompressZipArchiveCommand.cs +++ b/src/PSCompression/Commands/CompressZipArchiveCommand.cs @@ -12,14 +12,10 @@ namespace PSCompression.Commands; [Cmdlet(VerbsData.Compress, "ZipArchive")] [OutputType(typeof(FileInfo))] [Alias("ziparchive")] -public sealed class CompressZipArchiveCommand : PSCmdlet, IDisposable +public sealed class CompressZipArchiveCommand : CommandWithPathBase, IDisposable { private const FileShare s_sharemode = FileShare.ReadWrite | FileShare.Delete; - private bool _isLiteral; - - private string[] _paths = []; - private ZipArchive? _zip; private FileStream? _destination; @@ -28,35 +24,21 @@ public sealed class CompressZipArchiveCommand : PSCmdlet, IDisposable private readonly Queue _queue = new(); - [Parameter( - ParameterSetName = "Path", - Mandatory = true, - Position = 0, - ValueFromPipeline = true)] - [SupportsWildcards] - public string[] Path + private ZipArchiveMode ZipArchiveMode { - get => _paths; - set - { - _paths = value; - _isLiteral = false; - } + get => Force.IsPresent || Update.IsPresent + ? ZipArchiveMode.Update + : ZipArchiveMode.Create; } - [Parameter( - ParameterSetName = "LiteralPath", - Mandatory = true, - ValueFromPipelineByPropertyName = true)] - [Alias("PSPath")] - public string[] LiteralPath + private FileMode FileMode { - get => _paths; - set + get => (Update.IsPresent, Force.IsPresent) switch { - _paths = value; - _isLiteral = true; - } + (true, _) => FileMode.OpenOrCreate, + (_, true) => FileMode.Create, + _ => FileMode.CreateNew + }; } [Parameter(Mandatory = true, Position = 1)] @@ -82,14 +64,10 @@ public string[] LiteralPath protected override void BeginProcessing() { - if (!HasZipExtension(Destination)) - { - Destination += ".zip"; - } + Destination = ResolvePath(Destination).AddExtensionIfMissing(".zip"); try { - Destination = Destination.NormalizePath(isLiteral: true, this); string parent = Destination.GetParent(); if (!Directory.Exists(parent)) @@ -97,8 +75,8 @@ protected override void BeginProcessing() Directory.CreateDirectory(parent); } - _destination = File.Open(Destination, GetFileMode()); - _zip = new ZipArchive(_destination, GetZipMode()); + _destination = File.Open(Destination, FileMode); + _zip = new ZipArchive(_destination, ZipArchiveMode); } catch (Exception exception) { @@ -120,9 +98,9 @@ protected override void BeginProcessing() protected override void ProcessRecord() { Dbg.Assert(_zip is not null); - _queue.Clear(); - foreach (string path in _paths.NormalizePath(_isLiteral, this)) + + foreach (string path in EnumerateResolvedPaths()) { if (ShouldExclude(_excludePatterns, path)) { @@ -249,35 +227,6 @@ private void UpdateEntry( } } - private FileMode GetFileMode() - { - if (Update.IsPresent) - { - return FileMode.OpenOrCreate; - } - - if (Force.IsPresent) - { - return FileMode.Create; - } - - return FileMode.CreateNew; - } - - private ZipArchiveMode GetZipMode() - { - if (!Force.IsPresent && !Update.IsPresent) - { - return ZipArchiveMode.Create; - } - - return ZipArchiveMode.Update; - } - - private bool HasZipExtension(string path) => - System.IO.Path.GetExtension(path) - .Equals(".zip", StringComparison.InvariantCultureIgnoreCase); - private bool ItemIsDestination(string source, string destination) => source.Equals(destination, StringComparison.InvariantCultureIgnoreCase); @@ -316,5 +265,6 @@ public void Dispose() { _zip?.Dispose(); _destination?.Dispose(); + GC.SuppressFinalize(this); } } diff --git a/src/PSCompression/Commands/ExpandGzipArchiveCommand.cs b/src/PSCompression/Commands/ExpandGzipArchiveCommand.cs index 573a821..01dade8 100644 --- a/src/PSCompression/Commands/ExpandGzipArchiveCommand.cs +++ b/src/PSCompression/Commands/ExpandGzipArchiveCommand.cs @@ -15,13 +15,19 @@ namespace PSCompression.Commands; typeof(FileInfo), ParameterSetName = ["PathDestination", "LiteralPathDestination"])] [Alias("gzipfromfile")] -public sealed class ExpandGzipArchiveCommand : PSCmdlet, IDisposable +public sealed class ExpandGzipArchiveCommand : CommandWithPathBase, IDisposable { - private bool _isLiteral; - private FileStream? _destination; - private string[] _paths = []; + private FileMode FileMode + { + get => (Update.IsPresent, Force.IsPresent) switch + { + (true, _) => FileMode.Append, + (_, true) => FileMode.Create, + _ => FileMode.CreateNew + }; + } [Parameter( ParameterSetName = "Path", @@ -34,14 +40,10 @@ public sealed class ExpandGzipArchiveCommand : PSCmdlet, IDisposable Mandatory = true, ValueFromPipeline = true)] [SupportsWildcards] - public string[] Path + public override string[] Path { get => _paths; - set - { - _paths = value; - _isLiteral = false; - } + set => _paths = value; } [Parameter( @@ -53,14 +55,10 @@ public string[] Path Mandatory = true, ValueFromPipelineByPropertyName = true)] [Alias("PSPath")] - public string[] LiteralPath + public override string[] LiteralPath { get => _paths; - set - { - _paths = value; - _isLiteral = true; - } + set => _paths = value; } [Parameter( @@ -103,8 +101,7 @@ protected override void BeginProcessing() { try { - Destination = Destination.NormalizePath(isLiteral: true, this); - + Destination = ResolvePath(Destination); string parent = Destination.GetParent(); if (!Directory.Exists(parent)) @@ -112,7 +109,7 @@ protected override void BeginProcessing() Directory.CreateDirectory(parent); } - _destination = File.Open(Destination, GetMode()); + _destination = File.Open(Destination, FileMode); } catch (Exception exception) { @@ -123,13 +120,13 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - foreach (string path in _paths.NormalizePath(_isLiteral, this)) + foreach (string path in EnumerateResolvedPaths()) { if (!path.IsArchive()) { - WriteError(ExceptionHelpers.NotArchivePathError( + WriteError(ExceptionHelper.NotArchivePath( path, - _isLiteral ? nameof(LiteralPath) : nameof(Path))); + IsLiteral ? nameof(LiteralPath) : nameof(Path))); continue; } @@ -170,20 +167,9 @@ protected override void EndProcessing() } } - private FileMode GetMode() + public void Dispose() { - if (Update.IsPresent) - { - return FileMode.Append; - } - - if (Force.IsPresent) - { - return FileMode.Create; - } - - return FileMode.CreateNew; + _destination?.Dispose(); + GC.SuppressFinalize(this); } - - public void Dispose() => _destination?.Dispose(); } diff --git a/src/PSCompression/Commands/ExpandZipEntryCommand.cs b/src/PSCompression/Commands/ExpandZipEntryCommand.cs index ff4fea0..fb9245e 100644 --- a/src/PSCompression/Commands/ExpandZipEntryCommand.cs +++ b/src/PSCompression/Commands/ExpandZipEntryCommand.cs @@ -7,7 +7,7 @@ namespace PSCompression.Commands; [Cmdlet(VerbsData.Expand, "ZipEntry")] -[OutputType(typeof(FileSystemInfo), ParameterSetName = new[] { "PassThru" })] +[OutputType(typeof(FileSystemInfo))] public sealed class ExpandZipEntryCommand : PSCmdlet, IDisposable { private readonly ZipArchiveCache _cache = new(); @@ -22,27 +22,24 @@ public sealed class ExpandZipEntryCommand : PSCmdlet, IDisposable [Parameter] public SwitchParameter Force { get; set; } - [Parameter(ParameterSetName = "PassThru")] + [Parameter] public SwitchParameter PassThru { get; set; } protected override void BeginProcessing() { - Destination ??= SessionState.Path.CurrentFileSystemLocation.Path; + Destination = Destination is null + ? SessionState.Path.CurrentFileSystemLocation.Path + : Destination.ResolvePath(this); - try + if (Destination.IsArchive()) { - Destination = Destination.NormalizePath(isLiteral: true, this); - - if (File.Exists(Destination)) - { - ThrowTerminatingError(ExceptionHelpers.NotDirectoryPathError( - Destination, - nameof(Destination))); - } + ThrowTerminatingError( + ExceptionHelper.NotDirectoryPath(Destination, nameof(Destination))); } - catch (Exception exception) + + if (!Directory.Exists(Destination)) { - ThrowTerminatingError(exception.ToResolvePathError(Destination)); + Directory.CreateDirectory(Destination); } } @@ -64,7 +61,7 @@ protected override void ProcessRecord() if (isfile) { WriteObject(new FileInfo(path)); - return; + continue; } WriteObject(new DirectoryInfo(path)); @@ -77,5 +74,9 @@ protected override void ProcessRecord() } } - public void Dispose() => _cache?.Dispose(); + public void Dispose() + { + _cache?.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/src/PSCompression/Commands/GetZipEntryCommand.cs b/src/PSCompression/Commands/GetZipEntryCommand.cs index 147cb93..4e2703c 100644 --- a/src/PSCompression/Commands/GetZipEntryCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryCommand.cs @@ -11,53 +11,14 @@ namespace PSCompression.Commands; [Cmdlet(VerbsCommon.Get, "ZipEntry", DefaultParameterSetName = "Path")] [OutputType(typeof(ZipEntryDirectory), typeof(ZipEntryFile))] [Alias("gezip")] -public sealed class GetZipEntryCommand : PSCmdlet +public sealed class GetZipEntryCommand : CommandWithPathBase { - private bool _isLiteral; - - private bool _withInclude; - - private bool _withExclude; - - private string[] _paths = []; - - private readonly List _output = new(); + private readonly List _output = []; private WildcardPattern[]? _includePatterns; private WildcardPattern[]? _excludePatterns; - [Parameter( - ParameterSetName = "Path", - Position = 0, - Mandatory = true, - ValueFromPipeline = true)] - [SupportsWildcards] - public string[] Path - { - get => _paths; - set - { - _paths = value; - _isLiteral = false; - } - } - - [Parameter( - ParameterSetName = "LiteralPath", - Mandatory = true, - ValueFromPipelineByPropertyName = true)] - [Alias("PSPath")] - public string[] LiteralPath - { - get => _paths; - set - { - _paths = value; - _isLiteral = true; - } - } - [Parameter] public ZipEntryType? Type { get; set; } @@ -83,7 +44,6 @@ protected override void BeginProcessing() if (Exclude is not null) { - _withExclude = true; _excludePatterns = Exclude .Select(e => new WildcardPattern(e, wpoptions)) .ToArray(); @@ -91,7 +51,6 @@ protected override void BeginProcessing() if (Include is not null) { - _withInclude = true; _includePatterns = Include .Select(e => new WildcardPattern(e, wpoptions)) .ToArray(); @@ -100,13 +59,13 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - foreach (string path in _paths.NormalizePath(_isLiteral, this)) + foreach (string path in EnumerateResolvedPaths()) { if (!path.IsArchive()) { - WriteError(ExceptionHelpers.NotArchivePathError( + WriteError(ExceptionHelper.NotArchivePath( path, - _isLiteral ? nameof(LiteralPath) : nameof(Path))); + IsLiteral ? nameof(LiteralPath) : nameof(Path))); continue; } @@ -131,17 +90,12 @@ private IEnumerable GetEntries(string path) { bool isDirectory = string.IsNullOrEmpty(entry.Name); - if (SkipEntryType(isDirectory)) - { - continue; - } - - if (SkipInclude(entry.FullName)) + if (ShouldSkipEntry(isDirectory)) { continue; } - if (SkipExclude(entry.FullName)) + if (!ShouldInclude(entry) || ShouldExclude(entry)) { continue; } @@ -158,12 +112,42 @@ private IEnumerable GetEntries(string path) return _output.ZipEntrySort(); } - private bool SkipInclude(string path) => - _withInclude && !_includePatterns.Any(e => e.IsMatch(path)); + private static bool MatchAny( + ZipArchiveEntry entry, + WildcardPattern[] patterns) + { + foreach (WildcardPattern pattern in patterns) + { + if (pattern.IsMatch(entry.FullName)) + { + return true; + } + } + + return false; + } + + private bool ShouldInclude(ZipArchiveEntry entry) + { + if (_includePatterns is null) + { + return true; + } - private bool SkipExclude(string path) => - _withExclude && _excludePatterns.Any(e => e.IsMatch(path)); + return MatchAny(entry, _includePatterns); + } + + private bool ShouldExclude(ZipArchiveEntry entry) + { + if (_excludePatterns is null) + { + return false; + } + + return MatchAny(entry, _excludePatterns); + } - private bool SkipEntryType(bool isdir) => - (isdir && Type is ZipEntryType.Archive) || (!isdir && Type is ZipEntryType.Directory); + private bool ShouldSkipEntry(bool isDirectory) => + isDirectory && Type is ZipEntryType.Archive + || !isDirectory && Type is ZipEntryType.Directory; } diff --git a/src/PSCompression/Commands/GetZipEntryContentCommand.cs b/src/PSCompression/Commands/GetZipEntryContentCommand.cs index a70e357..7e66171 100644 --- a/src/PSCompression/Commands/GetZipEntryContentCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryContentCommand.cs @@ -31,7 +31,7 @@ public sealed class GetZipEntryContentCommand : PSCmdlet, IDisposable [Parameter(ParameterSetName = "Bytes")] [ValidateNotNullOrEmpty] - public int BufferSize { get; set; } = 128000; + public int BufferSize { get; set; } = 128_000; protected override void ProcessRecord() { @@ -55,24 +55,32 @@ private void ReadEntry(ZipEntryFile entry, ZipContentReader reader) { if (Raw.IsPresent) { - WriteObject(reader.ReadAllBytes(entry.RelativePath)); + WriteObject(reader.ReadAllBytes(entry)); return; } - reader.StreamBytes(entry.RelativePath, BufferSize, this); + WriteObject( + reader.StreamBytes(entry, BufferSize), + enumerateCollection: true); return; } if (Raw.IsPresent) { - WriteObject(reader.ReadToEnd(entry.RelativePath, Encoding)); + WriteObject(reader.ReadToEnd(entry, Encoding)); return; } - reader.StreamLines(entry.RelativePath, Encoding, this); + WriteObject( + reader.StreamLines(entry, Encoding), + enumerateCollection: true); } private ZipArchive GetOrAdd(ZipEntryFile entry) => _cache.GetOrAdd(entry); - public void Dispose() => _cache?.Dispose(); + public void Dispose() + { + _cache?.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/src/PSCompression/Commands/NewZipEntryCommand.cs b/src/PSCompression/Commands/NewZipEntryCommand.cs index 6259e0a..7677fc4 100644 --- a/src/PSCompression/Commands/NewZipEntryCommand.cs +++ b/src/PSCompression/Commands/NewZipEntryCommand.cs @@ -54,19 +54,13 @@ public string[]? EntryPath protected override void BeginProcessing() { - string path = Destination.NormalizePath( - isLiteral: true, - cmdlet: this, - throwOnInvalidProvider: true); - - if (!path.IsArchive()) + Destination = Destination.ResolvePath(this); + if (!Destination.IsArchive()) { ThrowTerminatingError( - ExceptionHelpers.NotArchivePathError(path, nameof(Destination))); + ExceptionHelper.NotArchivePath(Destination, nameof(Destination))); } - Destination = path; - try { _zip = ZipFile.Open(Destination, ZipArchiveMode.Update); @@ -82,9 +76,10 @@ protected override void BeginProcessing() { if (!Force.IsPresent) { - WriteError(DuplicatedEntryException - .Create(entry, Destination) - .ToDuplicatedEntryError()); + WriteError( + DuplicatedEntryException + .Create(entry, Destination) + .ToDuplicatedEntryError()); continue; } @@ -101,34 +96,31 @@ protected override void BeginProcessing() // else, we're on File ParameterSet, this can't be null Dbg.Assert(SourcePath is not null); // Create Entries from file here - string sourcePath = SourcePath.NormalizePath( - isLiteral: true, - cmdlet: this, - throwOnInvalidProvider: true); + SourcePath = SourcePath.ResolvePath(this); - if (!sourcePath.IsArchive()) + if (!SourcePath.IsArchive()) { - ThrowTerminatingError(ExceptionHelpers.NotArchivePathError( - path: sourcePath, - paramname: nameof(SourcePath))); + ThrowTerminatingError( + ExceptionHelper.NotArchivePath(SourcePath, nameof(SourcePath))); } using FileStream fileStream = File.Open( - path: sourcePath, + path: SourcePath, mode: FileMode.Open, access: FileAccess.Read, share: FileShare.ReadWrite); - EntryPath ??= [sourcePath.NormalizePath()]; + EntryPath ??= [SourcePath.NormalizePath()]; foreach (string entry in EntryPath) { if (_zip.TryGetEntry(entry, out ZipArchiveEntry? zipentry)) { if (!Force.IsPresent) { - WriteError(DuplicatedEntryException - .Create(entry, Destination) - .ToDuplicatedEntryError()); + WriteError( + DuplicatedEntryException + .Create(entry, Destination) + .ToDuplicatedEntryError()); continue; } @@ -200,8 +192,8 @@ protected override void EndProcessing() private IEnumerable GetResult() { using ZipArchive zip = ZipFile.OpenRead(Destination); - List _result = new(_entries.Count); + foreach (ZipArchiveEntry entry in _entries) { if (string.IsNullOrEmpty(entry.Name)) @@ -231,5 +223,6 @@ public void Dispose() } _zip?.Dispose(); + GC.SuppressFinalize(this); } } diff --git a/src/PSCompression/Commands/RemoveZipEntryCommand.cs b/src/PSCompression/Commands/RemoveZipEntryCommand.cs index f174e8c..c804f3b 100644 --- a/src/PSCompression/Commands/RemoveZipEntryCommand.cs +++ b/src/PSCompression/Commands/RemoveZipEntryCommand.cs @@ -31,5 +31,9 @@ protected override void ProcessRecord() } } - public void Dispose() => _cache?.Dispose(); + public void Dispose() + { + _cache?.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/src/PSCompression/Commands/RenameZipEntryCommand.cs b/src/PSCompression/Commands/RenameZipEntryCommand.cs index cf3446a..3a037a1 100644 --- a/src/PSCompression/Commands/RenameZipEntryCommand.cs +++ b/src/PSCompression/Commands/RenameZipEntryCommand.cs @@ -116,5 +116,9 @@ private void Rename(KeyValuePair> mapping) } } - public void Dispose() => _zipArchiveCache?.Dispose(); + public void Dispose() + { + _zipArchiveCache?.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/src/PSCompression/Commands/SetZipEntryContentCommand.cs b/src/PSCompression/Commands/SetZipEntryContentCommand.cs index bf1b692..ea9cc9f 100644 --- a/src/PSCompression/Commands/SetZipEntryContentCommand.cs +++ b/src/PSCompression/Commands/SetZipEntryContentCommand.cs @@ -1,9 +1,9 @@ using System; using System.Management.Automation; using System.Text; -using static PSCompression.Exceptions.ExceptionHelpers; +using PSCompression.Exceptions; -namespace PSCompression; +namespace PSCompression.Commands; [Cmdlet(VerbsCommon.Set, "ZipEntryContent", DefaultParameterSetName = "StringValue")] [OutputType(typeof(ZipEntryFile))] @@ -30,7 +30,7 @@ public sealed class SetZipEntryContentCommand : PSCmdlet, IDisposable public SwitchParameter Append { get; set; } [Parameter(ParameterSetName = "ByteStream")] - public int BufferSize { get; set; } = 128000; + public int BufferSize { get; set; } = 128_000; [Parameter] public SwitchParameter PassThru { get; set; } @@ -101,5 +101,9 @@ protected override void EndProcessing() } } - public void Dispose() => _zipWriter?.Dispose(); + public void Dispose() + { + _zipWriter?.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/src/PSCompression/Exceptions/ExceptionHelpers.cs b/src/PSCompression/Exceptions/ExceptionHelpers.cs index 01cdf32..6d26703 100644 --- a/src/PSCompression/Exceptions/ExceptionHelpers.cs +++ b/src/PSCompression/Exceptions/ExceptionHelpers.cs @@ -6,22 +6,29 @@ namespace PSCompression.Exceptions; -internal static class ExceptionHelpers +internal static class ExceptionHelper { private static readonly char[] s_InvalidFileNameChar = Path.GetInvalidFileNameChars(); private static readonly char[] s_InvalidPathChar = Path.GetInvalidPathChars(); - internal static ErrorRecord NotArchivePathError(string path, string paramname) => - new(new ArgumentException($"The specified path is a Directory: '{path}'.", paramname), + internal static ErrorRecord NotArchivePath(string path, string paramname) => + new( + new ArgumentException( + $"The specified path '{path}' does not exist or is a Directory.", + paramname), "NotArchivePath", ErrorCategory.InvalidArgument, path); - internal static ErrorRecord NotDirectoryPathError(string path, string paramname) => - new(new ArgumentException($"Destination path is an existing File: '{path}'.", paramname), + internal static ErrorRecord NotDirectoryPath(string path, string paramname) => + new( + new ArgumentException( + $"Destination path is an existing File: '{path}'.", paramname), "NotDirectoryPath", ErrorCategory.InvalidArgument, path); internal static ErrorRecord ToInvalidProviderError(this ProviderInfo provider, string path) => - new(new ArgumentException($"The resolved path '{path}' is not a FileSystem path but '{provider.Name}'."), + new( + new ArgumentException( + $"The resolved path '{path}' is not a FileSystem path but '{provider.Name}'."), "NotFileSystemPath", ErrorCategory.InvalidArgument, path); internal static ErrorRecord ToOpenError(this Exception exception, string path) => diff --git a/src/PSCompression/Extensions/PathExtensions.cs b/src/PSCompression/Extensions/PathExtensions.cs index ae78e5f..1525fd9 100644 --- a/src/PSCompression/Extensions/PathExtensions.cs +++ b/src/PSCompression/Extensions/PathExtensions.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; -using System.Linq; using System.Management.Automation; using System.Text.RegularExpressions; using Microsoft.PowerShell.Commands; @@ -22,84 +19,125 @@ public static class PathExtensions private const string _directorySeparator = "/"; - [ThreadStatic] - private static List? s_normalizedPaths; + // [ThreadStatic] + // private static List? s_normalizedPaths; + + // internal static string[] NormalizePath( + // this string[] paths, + // bool isLiteral, + // PSCmdlet cmdlet, + // bool throwOnInvalidProvider = false) + // { + // s_normalizedPaths ??= []; + // Collection resolvedPaths; + // ProviderInfo provider; + // s_normalizedPaths.Clear(); + + // foreach (string path in paths) + // { + // if (isLiteral) + // { + // string resolvedPath = cmdlet + // .SessionState.Path + // .GetUnresolvedProviderPathFromPSPath(path, out provider, out _); + + // if (!provider.IsFileSystem()) + // { + // if (throwOnInvalidProvider) + // { + // cmdlet.ThrowTerminatingError(provider.ToInvalidProviderError(path)); + // } + + // cmdlet.WriteError(provider.ToInvalidProviderError(path)); + // continue; + // } + + // s_normalizedPaths.Add(resolvedPath); + // continue; + // } + + // try + // { + // resolvedPaths = cmdlet.GetResolvedProviderPathFromPSPath(path, out provider); + + // foreach (string resolvedPath in resolvedPaths) + // { + // if (!provider.IsFileSystem()) + // { + // cmdlet.WriteError(provider.ToInvalidProviderError(resolvedPath)); + // continue; + // } + + // s_normalizedPaths.Add(resolvedPath); + // } + // } + // catch (Exception exception) + // { + + // cmdlet.WriteError(exception.ToResolvePathError(path)); + // } + // } + + // return [.. s_normalizedPaths]; + // } + + // internal static string NormalizePath( + // this string path, + // bool isLiteral, + // PSCmdlet cmdlet, + // bool throwOnInvalidProvider = false) => + // NormalizePath([path], isLiteral, cmdlet, throwOnInvalidProvider) + // .FirstOrDefault(); + + // internal static bool IsFileSystem(this ProviderInfo provider) => + // provider.ImplementingType == typeof(FileSystemProvider); + + internal static string ResolvePath(this string path, PSCmdlet cmdlet) + { + string resolved = cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath( + path: path, + provider: out ProviderInfo provider, + drive: out _); + + provider.Validate(path, throwOnInvalidProvider: true, cmdlet); + return resolved; + } - internal static string[] NormalizePath( - this string[] paths, - bool isLiteral, - PSCmdlet cmdlet, - bool throwOnInvalidProvider = false) + internal static bool Validate( + this ProviderInfo provider, + string path, + bool throwOnInvalidProvider, + PSCmdlet cmdlet) { - s_normalizedPaths ??= []; - Collection resolvedPaths; - ProviderInfo provider; - s_normalizedPaths.Clear(); + if (provider.ImplementingType == typeof(FileSystemProvider)) + { + return true; + } - foreach (string path in paths) + ErrorRecord error = provider.ToInvalidProviderError(path); + + if (throwOnInvalidProvider) { - if (isLiteral) - { - string resolvedPath = cmdlet - .SessionState.Path - .GetUnresolvedProviderPathFromPSPath(path, out provider, out _); - - if (!provider.IsFileSystem()) - { - if (throwOnInvalidProvider) - { - cmdlet.ThrowTerminatingError(provider.ToInvalidProviderError(path)); - } - - cmdlet.WriteError(provider.ToInvalidProviderError(path)); - continue; - } - - s_normalizedPaths.Add(resolvedPath); - continue; - } - - try - { - resolvedPaths = cmdlet.GetResolvedProviderPathFromPSPath(path, out provider); - - foreach (string resolvedPath in resolvedPaths) - { - if (!provider.IsFileSystem()) - { - cmdlet.WriteError(provider.ToInvalidProviderError(resolvedPath)); - continue; - } - - s_normalizedPaths.Add(resolvedPath); - } - } - catch (Exception exception) - { - - cmdlet.WriteError(exception.ToResolvePathError(path)); - } + cmdlet.ThrowTerminatingError(error); } - return [.. s_normalizedPaths]; + cmdlet.WriteError(error); + return false; } - internal static string NormalizePath( - this string path, - bool isLiteral, - PSCmdlet cmdlet, - bool throwOnInvalidProvider = false) => - NormalizePath([path], isLiteral, cmdlet, throwOnInvalidProvider) - .FirstOrDefault(); + internal static bool IsArchive(this string path) => File.Exists(path); - internal static bool IsFileSystem(this ProviderInfo provider) => - provider.ImplementingType == typeof(FileSystemProvider); + internal static string GetParent(this string path) => Path.GetDirectoryName(path); - internal static bool IsArchive(this string path) => - !File.GetAttributes(path).HasFlag(FileAttributes.Directory); + internal static string AddExtensionIfMissing(this string path, string extension) + { + if (!path.EndsWith(extension, StringComparison.InvariantCultureIgnoreCase)) + { + path += extension; + } - internal static string GetParent(this string path) => - Path.GetDirectoryName(path); + return path; + } internal static string NormalizeEntryPath(this string path) => s_reNormalize.Replace(path, _directorySeparator).TrimStart('/'); diff --git a/src/PSCompression/ZipArchiveCache.cs b/src/PSCompression/ZipArchiveCache.cs index 566895f..5dfd0ff 100644 --- a/src/PSCompression/ZipArchiveCache.cs +++ b/src/PSCompression/ZipArchiveCache.cs @@ -25,7 +25,7 @@ internal void TryAdd(ZipEntryBase entry) { if (!_cache.ContainsKey(entry.Source)) { - _cache[entry.Source] = entry.Open(_mode); + _cache[entry.Source] = entry.OpenZip(_mode); } } @@ -33,7 +33,7 @@ internal ZipArchive GetOrAdd(ZipEntryBase entry) { if (!_cache.ContainsKey(entry.Source)) { - _cache[entry.Source] = entry.Open(_mode); + _cache[entry.Source] = entry.OpenZip(_mode); } return _cache[entry.Source]; diff --git a/src/PSCompression/ZipContentOpsBase.cs b/src/PSCompression/ZipContentOpsBase.cs index 2e6d042..8b789ee 100644 --- a/src/PSCompression/ZipContentOpsBase.cs +++ b/src/PSCompression/ZipContentOpsBase.cs @@ -11,11 +11,9 @@ internal abstract class ZipContentOpsBase : IDisposable public bool Disposed { get; internal set; } - protected ZipContentOpsBase(ZipArchive zip) => - ZipArchive = zip; + protected ZipContentOpsBase(ZipArchive zip) => ZipArchive = zip; - protected ZipContentOpsBase() - { } + protected ZipContentOpsBase() { } protected virtual void Dispose(bool disposing) { diff --git a/src/PSCompression/ZipContentReader.cs b/src/PSCompression/ZipContentReader.cs index 2fe92c1..2e7afa8 100644 --- a/src/PSCompression/ZipContentReader.cs +++ b/src/PSCompression/ZipContentReader.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Linq; using System.Management.Automation; using System.Text; @@ -8,49 +8,46 @@ namespace PSCompression; internal sealed class ZipContentReader : ZipContentOpsBase { - internal ZipContentReader(ZipArchive zip) - : base(zip) - { } + internal ZipContentReader(ZipArchive zip) : base(zip) { } - private Stream GetStream(string entry) => - ZipArchive.GetEntry(entry).Open(); - - internal void StreamBytes(string entry, int bufferSize, PSCmdlet cmdlet) + internal IEnumerable StreamBytes(ZipEntryFile entry, int bufferSize) { - using Stream entryStream = GetStream(entry); - - int bytes; + using Stream entryStream = entry.Open(ZipArchive); _buffer ??= new byte[bufferSize]; + int bytes; while ((bytes = entryStream.Read(_buffer, 0, bufferSize)) > 0) { - cmdlet.WriteObject(_buffer.Take(bytes), enumerateCollection: true); + for (int i = 0; i < bytes; i++) + { + yield return _buffer[i]; + } } } - internal byte[] ReadAllBytes(string entry) + internal byte[] ReadAllBytes(ZipEntryFile entry) { - using Stream entryStream = GetStream(entry); + using Stream entryStream = entry.Open(ZipArchive); using MemoryStream mem = new(); entryStream.CopyTo(mem); return mem.ToArray(); } - internal void StreamLines(string entry, Encoding encoding, PSCmdlet cmdlet) + internal IEnumerable StreamLines(ZipEntryFile entry, Encoding encoding) { - using Stream entryStream = GetStream(entry); + using Stream entryStream = entry.Open(ZipArchive); using StreamReader reader = new(entryStream, encoding); while (!reader.EndOfStream) { - cmdlet.WriteObject(reader.ReadLine()); + yield return reader.ReadLine(); } } - internal string ReadToEnd(string entry, Encoding encoding) + internal string ReadToEnd(ZipEntryFile entry, Encoding encoding) { - using Stream entryStream = GetStream(entry); + using Stream entryStream = entry.Open(ZipArchive); using StreamReader reader = new(entryStream, encoding); return reader.ReadToEnd(); } diff --git a/src/PSCompression/ZipContentWriter.cs b/src/PSCompression/ZipContentWriter.cs index 62e9d50..b4a65ed 100644 --- a/src/PSCompression/ZipContentWriter.cs +++ b/src/PSCompression/ZipContentWriter.cs @@ -19,7 +19,7 @@ internal sealed class ZipContentWriter : ZipContentOpsBase internal ZipContentWriter(ZipEntryFile entry, bool append, int bufferSize) { ZipArchive = entry.OpenWrite(); - Stream = ZipArchive.GetEntry(entry.RelativePath).Open(); + Stream = entry.Open(ZipArchive); _buffer = new byte[bufferSize]; if (append) @@ -41,7 +41,7 @@ internal ZipContentWriter(ZipArchive zip, ZipArchiveEntry entry, Encoding encodi internal ZipContentWriter(ZipEntryFile entry, bool append, Encoding encoding) { ZipArchive = entry.OpenWrite(); - Stream = ZipArchive.GetEntry(entry.RelativePath).Open(); + Stream = entry.Open(ZipArchive); _writer = new StreamWriter(Stream, encoding); if (append) diff --git a/src/PSCompression/ZipEntryBase.cs b/src/PSCompression/ZipEntryBase.cs index 18e92bb..6509a59 100644 --- a/src/PSCompression/ZipEntryBase.cs +++ b/src/PSCompression/ZipEntryBase.cs @@ -42,8 +42,7 @@ public void Remove() zip.GetEntry(RelativePath)?.Delete(); } - internal void Remove(ZipArchive zip) => - zip.GetEntry(RelativePath)?.Delete(); + internal void Remove(ZipArchive zip) => zip.GetEntry(RelativePath)?.Delete(); internal static string Move( string sourceRelativePath, @@ -73,8 +72,7 @@ internal static string Move( return destination; } - internal ZipArchive Open(ZipArchiveMode mode) => - ZipFile.Open(Source, mode); + internal ZipArchive OpenZip(ZipArchiveMode mode) => ZipFile.Open(Source, mode); public FileSystemInfo ExtractTo(string destination, bool overwrite) { diff --git a/src/PSCompression/ZipEntryFile.cs b/src/PSCompression/ZipEntryFile.cs index e98fce3..89f9706 100644 --- a/src/PSCompression/ZipEntryFile.cs +++ b/src/PSCompression/ZipEntryFile.cs @@ -40,6 +40,8 @@ private static string GetRatio(long size, long compressedSize) public ZipArchive OpenWrite() => ZipFile.Open(Source, ZipArchiveMode.Update); + internal Stream Open(ZipArchive zip) => zip.GetEntry(RelativePath).Open(); + internal void Refresh() { using ZipArchive zip = OpenRead(); diff --git a/src/PSCompression/internal/_Format.cs b/src/PSCompression/internal/_Format.cs index 4c58aa1..d06d7c5 100644 --- a/src/PSCompression/internal/_Format.cs +++ b/src/PSCompression/internal/_Format.cs @@ -1,9 +1,7 @@ using System; using System.ComponentModel; using System.Globalization; -using System.IO; using System.Management.Automation; -using PSCompression.Extensions; namespace PSCompression.Internal; From 3593a9176fe9cda6dae1cf2de85fbe73040aae84 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 8 Jan 2025 19:48:00 -0300 Subject: [PATCH 02/15] lots of tidying up and organizing --- src/PSCompression/CommandWithPathBase.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PSCompression/CommandWithPathBase.cs b/src/PSCompression/CommandWithPathBase.cs index f0fa283..e0da45e 100644 --- a/src/PSCompression/CommandWithPathBase.cs +++ b/src/PSCompression/CommandWithPathBase.cs @@ -38,8 +38,7 @@ public virtual string[] LiteralPath set => _paths = value; } - protected IEnumerable EnumerateResolvedPaths( - bool throwOnInvalidProvider = false) + protected IEnumerable EnumerateResolvedPaths() { Collection resolvedPaths; ProviderInfo provider; @@ -53,7 +52,7 @@ protected IEnumerable EnumerateResolvedPaths( provider: out provider, drive: out _); - if (provider.Validate(path, throwOnInvalidProvider, this)) + if (provider.Validate(path, throwOnInvalidProvider: false, this)) { yield return resolved; } @@ -74,7 +73,7 @@ protected IEnumerable EnumerateResolvedPaths( foreach (string resolvedPath in resolvedPaths) { - if (!provider.Validate(path, throwOnInvalidProvider, this)) + if (!provider.Validate(path, throwOnInvalidProvider: false, this)) { yield return resolvedPath; } From f88d75fa8855a2914253c968d3539d8cfd8a8ce4 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 10:23:52 -0300 Subject: [PATCH 03/15] upload test zip --- .gitignore | 1 + assets/test.zip | Bin 0 -> 23483 bytes 2 files changed, 1 insertion(+) create mode 100644 assets/test.zip diff --git a/.gitignore b/.gitignore index 4a89b4b..03ede8b 100644 --- a/.gitignore +++ b/.gitignore @@ -271,3 +271,4 @@ tools/Modules test.settings.json tests/integration/.vagrant tests/integration/cert_setup +!assets/*.zip diff --git a/assets/test.zip b/assets/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..d2ed9ad7d38411922937f3d9e645d3ab145e9fb3 GIT binary patch literal 23483 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 Ef9enFoB#j- literal 0 HcmV?d00001 From cbf7823ded45d31403d62bf5969c30c40d4854ce Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 14:45:44 -0300 Subject: [PATCH 04/15] almost ready to roll --- assets/test.zip | Bin 23483 -> 0 bytes module/PSCompression.psd1 | 2 +- .../Commands/GetZipEntryCommand.cs | 53 +++++++++++----- .../Commands/NewZipEntryCommand.cs | 6 +- .../Commands/RenameZipEntryCommand.cs | 1 + .../Exceptions/ExceptionHelpers.cs | 14 ++++- .../Extensions/ZipEntryExtensions.cs | 13 +--- src/PSCompression/ZipArchiveCache.cs | 5 +- src/PSCompression/ZipContentOpsBase.cs | 10 +-- src/PSCompression/ZipContentReader.cs | 9 ++- src/PSCompression/ZipContentWriter.cs | 34 +++++----- src/PSCompression/ZipEntryBase.cs | 59 ++++++++++++------ src/PSCompression/ZipEntryDirectory.cs | 5 ++ src/PSCompression/ZipEntryFile.cs | 22 ++++++- src/PSCompression/ZipEntryMoveCache.cs | 11 ++-- 15 files changed, 157 insertions(+), 87 deletions(-) delete mode 100644 assets/test.zip diff --git a/assets/test.zip b/assets/test.zip deleted file mode 100644 index d2ed9ad7d38411922937f3d9e645d3ab145e9fb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23483 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 Ef9enFoB#j- diff --git a/module/PSCompression.psd1 b/module/PSCompression.psd1 index 25c3b98..6c84776 100644 --- a/module/PSCompression.psd1 +++ b/module/PSCompression.psd1 @@ -11,7 +11,7 @@ RootModule = 'bin/netstandard2.0/PSCompression.dll' # Version number of this module. - ModuleVersion = '2.0.10' + ModuleVersion = '2.1.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/src/PSCompression/Commands/GetZipEntryCommand.cs b/src/PSCompression/Commands/GetZipEntryCommand.cs index 4e2703c..892fe1f 100644 --- a/src/PSCompression/Commands/GetZipEntryCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryCommand.cs @@ -5,6 +5,7 @@ using System.Management.Automation; using PSCompression.Extensions; using PSCompression.Exceptions; +using System.IO; namespace PSCompression.Commands; @@ -13,6 +14,13 @@ namespace PSCompression.Commands; [Alias("gezip")] public sealed class GetZipEntryCommand : CommandWithPathBase { + [Parameter( + ParameterSetName = "Stream", + Mandatory = true, + ValueFromPipelineByPropertyName = true)] + [Alias("RawContentStream")] + public Stream? Stream { get; set; } + private readonly List _output = []; private WildcardPattern[]? _includePatterns; @@ -59,20 +67,43 @@ protected override void BeginProcessing() protected override void ProcessRecord() { + if (Stream is not null) + { + ZipEntryBase CreateFromStream(ZipArchiveEntry entry, bool isDirectory) => + isDirectory + ? new ZipEntryDirectory(entry, Stream) + : new ZipEntryFile(entry, Stream); + + using ZipArchive zip = new(Stream, ZipArchiveMode.Read, true); + WriteObject( + EnumerateEntries(zip, CreateFromStream), + enumerateCollection: true); + return; + } + foreach (string path in EnumerateResolvedPaths()) { + ZipEntryBase CreateFromFile(ZipArchiveEntry entry, bool isDirectory) => + isDirectory + ? new ZipEntryDirectory(entry, path) + : new ZipEntryFile(entry, path); + if (!path.IsArchive()) { - WriteError(ExceptionHelper.NotArchivePath( - path, - IsLiteral ? nameof(LiteralPath) : nameof(Path))); + WriteError( + ExceptionHelper.NotArchivePath( + path, + IsLiteral ? nameof(LiteralPath) : nameof(Path))); continue; } try { - WriteObject(GetEntries(path), enumerateCollection: true); + using ZipArchive zip = ZipFile.OpenRead(path); + WriteObject( + EnumerateEntries(zip, CreateFromFile), + enumerateCollection: true); } catch (Exception exception) { @@ -81,11 +112,11 @@ protected override void ProcessRecord() } } - private IEnumerable GetEntries(string path) + private IEnumerable EnumerateEntries( + ZipArchive zip, + Func createMethod) { - using ZipArchive zip = ZipFile.OpenRead(path); _output.Clear(); - foreach (ZipArchiveEntry entry in zip.Entries) { bool isDirectory = string.IsNullOrEmpty(entry.Name); @@ -100,13 +131,7 @@ private IEnumerable GetEntries(string path) continue; } - if (isDirectory) - { - _output.Add(new ZipEntryDirectory(entry, path)); - continue; - } - - _output.Add(new ZipEntryFile(entry, path)); + _output.Add(createMethod(entry, isDirectory)); } return _output.ZipEntrySort(); diff --git a/src/PSCompression/Commands/NewZipEntryCommand.cs b/src/PSCompression/Commands/NewZipEntryCommand.cs index 7677fc4..f88b1e1 100644 --- a/src/PSCompression/Commands/NewZipEntryCommand.cs +++ b/src/PSCompression/Commands/NewZipEntryCommand.cs @@ -58,7 +58,9 @@ protected override void BeginProcessing() if (!Destination.IsArchive()) { ThrowTerminatingError( - ExceptionHelper.NotArchivePath(Destination, nameof(Destination))); + ExceptionHelper.NotArchivePath( + Destination, + nameof(Destination))); } try @@ -72,7 +74,7 @@ protected override void BeginProcessing() // We can create the entries here and go the process block foreach (string entry in EntryPath) { - if (_zip.TryGetEntry(entry, out ZipArchiveEntry zipentry)) + if (_zip.TryGetEntry(entry, out ZipArchiveEntry? zipentry)) { if (!Force.IsPresent) { diff --git a/src/PSCompression/Commands/RenameZipEntryCommand.cs b/src/PSCompression/Commands/RenameZipEntryCommand.cs index 3a037a1..cbef420 100644 --- a/src/PSCompression/Commands/RenameZipEntryCommand.cs +++ b/src/PSCompression/Commands/RenameZipEntryCommand.cs @@ -49,6 +49,7 @@ protected override void ProcessRecord() try { + ZipEntry.ThrowIfFromStream(); NewName.ThrowIfInvalidNameChar(); _zipArchiveCache.TryAdd(ZipEntry); _moveCache.AddEntry(ZipEntry, NewName); diff --git a/src/PSCompression/Exceptions/ExceptionHelpers.cs b/src/PSCompression/Exceptions/ExceptionHelpers.cs index 6d26703..72eadcc 100644 --- a/src/PSCompression/Exceptions/ExceptionHelpers.cs +++ b/src/PSCompression/Exceptions/ExceptionHelpers.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Management.Automation; @@ -65,7 +66,7 @@ internal static void ThrowIfNotFound( this ZipArchive zip, string path, string source, - out ZipArchiveEntry entry) + [NotNull] out ZipArchiveEntry? entry) { if (!zip.TryGetEntry(path, out entry)) { @@ -78,7 +79,7 @@ internal static void ThrowIfDuplicate( string path, string source) { - if (zip.TryGetEntry(path, out ZipArchiveEntry _)) + if (zip.TryGetEntry(path, out ZipArchiveEntry? _)) { throw DuplicatedEntryException.Create(path, source); } @@ -100,4 +101,13 @@ internal static void ThrowIfInvalidPathChar(this string path) $"Path: '{path}' contains invalid path characters."); } } + + internal static void ThrowIfFromStream(this ZipEntryBase entry) + { + if (entry.FromStream) + { + throw new NotSupportedException( + "The operation is not supported for entries created from input Stream."); + } + } } diff --git a/src/PSCompression/Extensions/ZipEntryExtensions.cs b/src/PSCompression/Extensions/ZipEntryExtensions.cs index df26592..3cb1be5 100644 --- a/src/PSCompression/Extensions/ZipEntryExtensions.cs +++ b/src/PSCompression/Extensions/ZipEntryExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Text.RegularExpressions; @@ -43,19 +44,9 @@ internal static ZipArchiveEntry CreateEntryFromFile( internal static bool TryGetEntry( this ZipArchive zip, string path, - out ZipArchiveEntry entry) => + [NotNullWhen(true)] out ZipArchiveEntry? entry) => (entry = zip.GetEntry(path)) is not null; - internal static string Move( - this ZipEntryBase entry, - string destination, - ZipArchive zip) => - ZipEntryBase.Move( - sourceRelativePath: entry.RelativePath, - destination: destination, - sourceZipPath: entry.Source, - zip: zip); - internal static (string, bool) ExtractTo( this ZipEntryBase entryBase, ZipArchive zip, diff --git a/src/PSCompression/ZipArchiveCache.cs b/src/PSCompression/ZipArchiveCache.cs index 5dfd0ff..11ab3cf 100644 --- a/src/PSCompression/ZipArchiveCache.cs +++ b/src/PSCompression/ZipArchiveCache.cs @@ -10,7 +10,7 @@ internal sealed class ZipArchiveCache : IDisposable private readonly ZipArchiveMode _mode = ZipArchiveMode.Read; - internal ZipArchiveCache() => _cache = new(); + internal ZipArchiveCache() => _cache = []; internal ZipArchiveCache(ZipArchiveMode mode) { @@ -18,8 +18,7 @@ internal ZipArchiveCache(ZipArchiveMode mode) _mode = mode; } - internal ZipArchive this[string source] => - _cache[source]; + internal ZipArchive this[string source] => _cache[source]; internal void TryAdd(ZipEntryBase entry) { diff --git a/src/PSCompression/ZipContentOpsBase.cs b/src/PSCompression/ZipContentOpsBase.cs index 8b789ee..71b9e44 100644 --- a/src/PSCompression/ZipContentOpsBase.cs +++ b/src/PSCompression/ZipContentOpsBase.cs @@ -3,23 +3,19 @@ namespace PSCompression; -internal abstract class ZipContentOpsBase : IDisposable +internal abstract class ZipContentOpsBase(ZipArchive zip) : IDisposable { - public virtual ZipArchive ZipArchive { get; } = default!; + protected ZipArchive _zip = zip; protected byte[]? _buffer; public bool Disposed { get; internal set; } - protected ZipContentOpsBase(ZipArchive zip) => ZipArchive = zip; - - protected ZipContentOpsBase() { } - protected virtual void Dispose(bool disposing) { if (disposing && !Disposed) { - ZipArchive?.Dispose(); + _zip?.Dispose(); Disposed = true; } } diff --git a/src/PSCompression/ZipContentReader.cs b/src/PSCompression/ZipContentReader.cs index 2e7afa8..1dff6e2 100644 --- a/src/PSCompression/ZipContentReader.cs +++ b/src/PSCompression/ZipContentReader.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Management.Automation; using System.Text; namespace PSCompression; @@ -12,7 +11,7 @@ internal ZipContentReader(ZipArchive zip) : base(zip) { } internal IEnumerable StreamBytes(ZipEntryFile entry, int bufferSize) { - using Stream entryStream = entry.Open(ZipArchive); + using Stream entryStream = entry.Open(_zip); _buffer ??= new byte[bufferSize]; int bytes; @@ -27,7 +26,7 @@ internal IEnumerable StreamBytes(ZipEntryFile entry, int bufferSize) internal byte[] ReadAllBytes(ZipEntryFile entry) { - using Stream entryStream = entry.Open(ZipArchive); + using Stream entryStream = entry.Open(_zip); using MemoryStream mem = new(); entryStream.CopyTo(mem); @@ -36,7 +35,7 @@ internal byte[] ReadAllBytes(ZipEntryFile entry) internal IEnumerable StreamLines(ZipEntryFile entry, Encoding encoding) { - using Stream entryStream = entry.Open(ZipArchive); + using Stream entryStream = entry.Open(_zip); using StreamReader reader = new(entryStream, encoding); while (!reader.EndOfStream) @@ -47,7 +46,7 @@ internal IEnumerable StreamLines(ZipEntryFile entry, Encoding encoding) internal string ReadToEnd(ZipEntryFile entry, Encoding encoding) { - using Stream entryStream = entry.Open(ZipArchive); + using Stream entryStream = entry.Open(_zip); using StreamReader reader = new(entryStream, encoding); return reader.ReadToEnd(); } diff --git a/src/PSCompression/ZipContentWriter.cs b/src/PSCompression/ZipContentWriter.cs index b4a65ed..6945e20 100644 --- a/src/PSCompression/ZipContentWriter.cs +++ b/src/PSCompression/ZipContentWriter.cs @@ -8,41 +8,39 @@ internal sealed class ZipContentWriter : ZipContentOpsBase { private int _index; - public override ZipArchive ZipArchive { get; } - private readonly StreamWriter? _writer; - private readonly Stream Stream; + private readonly Stream _stream; private bool _disposed; internal ZipContentWriter(ZipEntryFile entry, bool append, int bufferSize) + : base(entry.OpenWrite()) { - ZipArchive = entry.OpenWrite(); - Stream = entry.Open(ZipArchive); + _stream = entry.Open(_zip); _buffer = new byte[bufferSize]; if (append) { - Stream.Seek(0, SeekOrigin.End); + _stream.Seek(0, SeekOrigin.End); return; } - Stream.SetLength(0); + _stream.SetLength(0); } internal ZipContentWriter(ZipArchive zip, ZipArchiveEntry entry, Encoding encoding) + : base(zip) { - ZipArchive = zip; - Stream = entry.Open(); - _writer = new StreamWriter(Stream, encoding); + _stream = entry.Open(); + _writer = new StreamWriter(_stream, encoding); } internal ZipContentWriter(ZipEntryFile entry, bool append, Encoding encoding) + : base(entry.OpenWrite()) { - ZipArchive = entry.OpenWrite(); - Stream = entry.Open(ZipArchive); - _writer = new StreamWriter(Stream, encoding); + _stream = entry.Open(_zip); + _writer = new StreamWriter(_stream, encoding); if (append) { @@ -77,7 +75,7 @@ internal void WriteBytes(byte[] bytes) { if (_index == _buffer.Length) { - Stream.Write(_buffer, 0, _index); + _stream.Write(_buffer, 0, _index); _index = 0; } @@ -89,9 +87,9 @@ public void Flush() { if (_index > 0 && _buffer is not null) { - Stream.Write(_buffer, 0, _index); + _stream.Write(_buffer, 0, _index); _index = 0; - Stream.Flush(); + _stream.Flush(); } if (_writer is { BaseStream.CanWrite: true }) @@ -108,7 +106,7 @@ public void Close() return; } - Stream.Close(); + _stream.Close(); } protected override void Dispose(bool disposing) @@ -123,7 +121,7 @@ protected override void Dispose(bool disposing) finally { _writer?.Dispose(); - Stream.Dispose(); + _stream.Dispose(); _disposed = true; base.Dispose(disposing); } diff --git a/src/PSCompression/ZipEntryBase.cs b/src/PSCompression/ZipEntryBase.cs index 6509a59..ab8ed40 100644 --- a/src/PSCompression/ZipEntryBase.cs +++ b/src/PSCompression/ZipEntryBase.cs @@ -6,43 +6,63 @@ namespace PSCompression; -public abstract class ZipEntryBase +public abstract class ZipEntryBase(ZipArchiveEntry entry, string source) { protected string? _formatDirectoryPath; + protected Stream? _stream; + + internal bool FromStream { get => _stream is not null; } + internal abstract string FormatDirectoryPath { get; } - public string Source { get; } + public string Source { get; } = source; - public string Name { get; protected set; } + public string Name { get; protected set; } = entry.Name; - public string RelativePath { get; } + public string RelativePath { get; } = entry.FullName; - public DateTime LastWriteTime { get; } + public DateTime LastWriteTime { get; } = entry.LastWriteTime.LocalDateTime; - public long Length { get; internal set; } + public long Length { get; internal set; } = entry.Length; - public long CompressedLength { get; internal set; } + public long CompressedLength { get; internal set; } = entry.CompressedLength; public abstract ZipEntryType Type { get; } - protected ZipEntryBase(ZipArchiveEntry entry, string source) + protected ZipEntryBase(ZipArchiveEntry entry, Stream? stream) + : this(entry, $"InputStream.{Guid.NewGuid()}") { - Source = source; - Name = entry.Name; - RelativePath = entry.FullName; - LastWriteTime = entry.LastWriteTime.LocalDateTime; - Length = entry.Length; - CompressedLength = entry.CompressedLength; + _stream = stream; } public void Remove() { - using ZipArchive zip = ZipFile.Open(Source, ZipArchiveMode.Update); - zip.GetEntry(RelativePath)?.Delete(); + this.ThrowIfFromStream(); + + using ZipArchive zip = ZipFile.Open( + Source, + ZipArchiveMode.Update); + + zip.ThrowIfNotFound( + path: RelativePath, + source: Source, + out ZipArchiveEntry entry); + + entry.Delete(); } - internal void Remove(ZipArchive zip) => zip.GetEntry(RelativePath)?.Delete(); + internal void Remove(ZipArchive zip) + { + this.ThrowIfFromStream(); + + zip.ThrowIfNotFound( + path: RelativePath, + source: Source, + out ZipArchiveEntry entry); + + entry.Delete(); + } internal static string Move( string sourceRelativePath, @@ -72,7 +92,10 @@ internal static string Move( return destination; } - internal ZipArchive OpenZip(ZipArchiveMode mode) => ZipFile.Open(Source, mode); + internal ZipArchive OpenZip(ZipArchiveMode mode) => + _stream is null + ? ZipFile.Open(Source, mode) + : new ZipArchive(_stream, mode, true); public FileSystemInfo ExtractTo(string destination, bool overwrite) { diff --git a/src/PSCompression/ZipEntryDirectory.cs b/src/PSCompression/ZipEntryDirectory.cs index 7673ee5..86ca69f 100644 --- a/src/PSCompression/ZipEntryDirectory.cs +++ b/src/PSCompression/ZipEntryDirectory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Compression; using System.Linq; using PSCompression.Extensions; @@ -23,6 +24,10 @@ internal ZipEntryDirectory(ZipArchiveEntry entry, string source) Name = entry.GetDirectoryName(); } + internal ZipEntryDirectory(ZipArchiveEntry entry, Stream? stream) + : base(entry, stream) + { } + internal IEnumerable GetChilds(ZipArchive zip) => zip.Entries.Where(e => !string.Equals(e.FullName, RelativePath, _comparer) diff --git a/src/PSCompression/ZipEntryFile.cs b/src/PSCompression/ZipEntryFile.cs index 89f9706..c093cbb 100644 --- a/src/PSCompression/ZipEntryFile.cs +++ b/src/PSCompression/ZipEntryFile.cs @@ -1,5 +1,6 @@ using System.IO; using System.IO.Compression; +using PSCompression.Exceptions; using PSCompression.Extensions; namespace PSCompression; @@ -24,6 +25,10 @@ internal ZipEntryFile(ZipArchiveEntry entry, string source) : base(entry, source) { } + internal ZipEntryFile(ZipArchiveEntry entry, Stream? stream) + : base(entry, stream) + { } + private static string GetRatio(long size, long compressedSize) { float compressedRatio = (float)compressedSize / size; @@ -38,12 +43,25 @@ private static string GetRatio(long size, long compressedSize) public ZipArchive OpenRead() => ZipFile.OpenRead(Source); - public ZipArchive OpenWrite() => ZipFile.Open(Source, ZipArchiveMode.Update); + public ZipArchive OpenWrite() + { + this.ThrowIfFromStream(); + return ZipFile.Open(Source, ZipArchiveMode.Update); + } - internal Stream Open(ZipArchive zip) => zip.GetEntry(RelativePath).Open(); + internal Stream Open(ZipArchive zip) + { + zip.ThrowIfNotFound( + path: RelativePath, + source: Source, + out ZipArchiveEntry entry); + + return entry.Open(); + } internal void Refresh() { + this.ThrowIfFromStream(); using ZipArchive zip = OpenRead(); Refresh(zip); } diff --git a/src/PSCompression/ZipEntryMoveCache.cs b/src/PSCompression/ZipEntryMoveCache.cs index 2e0f389..cd92190 100644 --- a/src/PSCompression/ZipEntryMoveCache.cs +++ b/src/PSCompression/ZipEntryMoveCache.cs @@ -31,8 +31,7 @@ private Dictionary WithSource(ZipEntryBase entry) internal bool IsDirectoryEntry(string source, string path) => _cache[source].TryGetValue(path, out EntryWithPath entryWithPath) - ? entryWithPath.ZipEntry.Type is ZipEntryType.Directory - : false; + && entryWithPath.ZipEntry.Type is ZipEntryType.Directory; internal void AddEntry(ZipEntryBase entry, string newname) => WithSource(entry).Add(entry.RelativePath, new(entry, newname)); @@ -43,7 +42,11 @@ internal void AddEntry(ZipEntryBase entry, string newname) => { foreach ((string path, EntryWithPath entryWithPath) in source.Value) { - yield return (source.Key, new(_mappings[source.Key][path], entryWithPath.ZipEntry.Type)); + yield return ( + source.Key, + new PathWithType( + _mappings[source.Key][path], + entryWithPath.ZipEntry.Type)); } } } @@ -64,7 +67,7 @@ private Dictionary GetChildMappings( Dictionary pathChanges) { string newpath; - Dictionary result = new(); + Dictionary result = []; foreach (var pair in pathChanges.OrderByDescending(e => e.Key)) { From eef8b5d8ba0019c4f378bef021d9a9109e86cd8a Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 15:31:56 -0300 Subject: [PATCH 05/15] good shit --- src/PSCompression/Commands/GetZipEntryCommand.cs | 2 ++ src/PSCompression/Commands/RemoveZipEntryCommand.cs | 4 ++++ src/PSCompression/Commands/RenameZipEntryCommand.cs | 4 ++++ src/PSCompression/Exceptions/ExceptionHelpers.cs | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/PSCompression/Commands/GetZipEntryCommand.cs b/src/PSCompression/Commands/GetZipEntryCommand.cs index 892fe1f..318a5b7 100644 --- a/src/PSCompression/Commands/GetZipEntryCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryCommand.cs @@ -16,7 +16,9 @@ public sealed class GetZipEntryCommand : CommandWithPathBase { [Parameter( ParameterSetName = "Stream", + Position = 0, Mandatory = true, + ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [Alias("RawContentStream")] public Stream? Stream { get; set; } diff --git a/src/PSCompression/Commands/RemoveZipEntryCommand.cs b/src/PSCompression/Commands/RemoveZipEntryCommand.cs index c804f3b..5690685 100644 --- a/src/PSCompression/Commands/RemoveZipEntryCommand.cs +++ b/src/PSCompression/Commands/RemoveZipEntryCommand.cs @@ -24,6 +24,10 @@ protected override void ProcessRecord() entry.Remove(_cache.GetOrAdd(entry)); } } + catch (NotSupportedException exception) + { + ThrowTerminatingError(exception.ToStreamOpenError(entry)); + } catch (Exception exception) { WriteError(exception.ToOpenError(entry.Source)); diff --git a/src/PSCompression/Commands/RenameZipEntryCommand.cs b/src/PSCompression/Commands/RenameZipEntryCommand.cs index cbef420..38683da 100644 --- a/src/PSCompression/Commands/RenameZipEntryCommand.cs +++ b/src/PSCompression/Commands/RenameZipEntryCommand.cs @@ -54,6 +54,10 @@ protected override void ProcessRecord() _zipArchiveCache.TryAdd(ZipEntry); _moveCache.AddEntry(ZipEntry, NewName); } + catch (NotSupportedException exception) + { + ThrowTerminatingError(exception.ToStreamOpenError(ZipEntry)); + } catch (InvalidNameException exception) { WriteError(exception.ToInvalidNameError(NewName)); diff --git a/src/PSCompression/Exceptions/ExceptionHelpers.cs b/src/PSCompression/Exceptions/ExceptionHelpers.cs index 72eadcc..d528414 100644 --- a/src/PSCompression/Exceptions/ExceptionHelpers.cs +++ b/src/PSCompression/Exceptions/ExceptionHelpers.cs @@ -41,7 +41,7 @@ internal static ErrorRecord ToResolvePathError(this Exception exception, string internal static ErrorRecord ToExtractEntryError(this Exception exception, ZipEntryBase entry) => new(exception, "ExtractEntry", ErrorCategory.NotSpecified, entry); - internal static ErrorRecord ToStreamOpenError(this Exception exception, ZipEntryFile entry) => + internal static ErrorRecord ToStreamOpenError(this Exception exception, ZipEntryBase entry) => new(exception, "StreamOpen", ErrorCategory.NotSpecified, entry); internal static ErrorRecord ToStreamOpenError(this Exception exception, string path) => From 9b1fdfba5e57379a58829e3770aaed1c12103b2f Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 16:23:16 -0300 Subject: [PATCH 06/15] good shit --- src/PSCompression/Commands/GetZipEntryCommand.cs | 15 +++++++++------ tests/ZipEntryCmdlets.tests.ps1 | 12 +++++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/PSCompression/Commands/GetZipEntryCommand.cs b/src/PSCompression/Commands/GetZipEntryCommand.cs index 318a5b7..ef0812a 100644 --- a/src/PSCompression/Commands/GetZipEntryCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryCommand.cs @@ -78,7 +78,7 @@ ZipEntryBase CreateFromStream(ZipArchiveEntry entry, bool isDirectory) => using ZipArchive zip = new(Stream, ZipArchiveMode.Read, true); WriteObject( - EnumerateEntries(zip, CreateFromStream), + GetEntries(zip, CreateFromStream), enumerateCollection: true); return; } @@ -102,10 +102,13 @@ ZipEntryBase CreateFromFile(ZipArchiveEntry entry, bool isDirectory) => try { - using ZipArchive zip = ZipFile.OpenRead(path); - WriteObject( - EnumerateEntries(zip, CreateFromFile), - enumerateCollection: true); + IEnumerable entries; + using (ZipArchive zip = ZipFile.OpenRead(path)) + { + entries = GetEntries(zip, CreateFromFile); + } + + WriteObject(entries, enumerateCollection: true); } catch (Exception exception) { @@ -114,7 +117,7 @@ ZipEntryBase CreateFromFile(ZipArchiveEntry entry, bool isDirectory) => } } - private IEnumerable EnumerateEntries( + private IEnumerable GetEntries( ZipArchive zip, Func createMethod) { diff --git a/tests/ZipEntryCmdlets.tests.ps1 b/tests/ZipEntryCmdlets.tests.ps1 index 094f855..0383678 100644 --- a/tests/ZipEntryCmdlets.tests.ps1 +++ b/tests/ZipEntryCmdlets.tests.ps1 @@ -10,7 +10,8 @@ Describe 'ZipEntry Cmdlets' { BeforeAll { $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force $file = New-Item ([System.IO.Path]::Combine($TestDrive, 'someFile.txt')) -ItemType File -Value 'foo' - $zip, $file | Out-Null + $uri = 'https://www.powershellgallery.com/api/v2/package/PSCompression' + $zip, $file, $uri | Out-Null } Context 'New-ZipEntry' -Tag 'New-ZipEntry' { @@ -113,8 +114,13 @@ Describe 'ZipEntry Cmdlets' { Context 'Get-ZipEntry' -Tag 'Get-ZipEntry' { It 'Can list entries in a zip archive' { - @($zip | Get-ZipEntry).Count -gt 0 | - Should -BeGreaterThan 0 + $zip | Get-ZipEntry | + Should -BeOfType ([PSCompression.ZipEntryBase]) + } + + It 'Can list entries from a Stream' { + Invoke-WebRequest $uri | Get-ZipEntry | + Should -BeOfType ([PSCompression.ZipEntryBase]) } It 'Should throw when not targetting a FileSystem Provider Path' { From e20e277d7d2f816f450878793d83c79f80a046f6 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 17:06:53 -0300 Subject: [PATCH 07/15] done pester tests --- tests/ZipEntryCmdlets.tests.ps1 | 37 ++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/ZipEntryCmdlets.tests.ps1 b/tests/ZipEntryCmdlets.tests.ps1 index 0383678..c654e4d 100644 --- a/tests/ZipEntryCmdlets.tests.ps1 +++ b/tests/ZipEntryCmdlets.tests.ps1 @@ -164,7 +164,14 @@ Describe 'ZipEntry Cmdlets' { Should -BeOfType ([string]) } - It 'Should not throw when an instance wrapped in PSObject is passed as Encdoing argument' { + It 'Can read content from zip file entries created from input Stream' { + Invoke-WebRequest $uri | Get-ZipEntry -Type Archive -Include *.psd1 | + Get-ZipEntryContent -Raw | + Invoke-Expression | + Should -BeOfType ([System.Collections.IDictionary]) + } + + It 'Should not throw when an instance wrapped in PSObject is passed as Encoding argument' { $enc = Write-Output utf8 { $zip | Get-ZipEntry -Type Archive | Get-ZipEntryContent -Encoding $enc } | Should -Not -Throw @@ -209,6 +216,11 @@ Describe 'ZipEntry Cmdlets' { $zip | Get-ZipEntry | Should -Not -BeOfType ([PSCompression.ZipEntryFile]) } + It 'Should throw if trying to remove entries created from input Stream' { + { Invoke-WebRequest $uri | Get-ZipEntry | Remove-ZipEntry } | + Should -Throw -ExceptionType ([System.NotSupportedException]) + } + It 'Can remove directory entries' { $entries = $zip | Get-ZipEntry -Type Directory { Remove-ZipEntry -InputObject $entries } | Should -Not -Throw @@ -243,6 +255,12 @@ Describe 'ZipEntry Cmdlets' { Should -BeExactly ($content -join [System.Environment]::NewLine) } + It 'Should throw if trying to write content to entries created from input Stream' { + $entry = Invoke-WebRequest $uri | Get-ZipEntry -Include *.psd1 + { 'test' | Set-ZipEntryContent $entry } | + Should -Throw -ExceptionType ([System.NotSupportedException]) + } + It 'Can append content to a zip file entry' { $newContent = $content + $content $content | Set-ZipEntryContent $entry -Append @@ -300,6 +318,11 @@ Describe 'ZipEntry Cmdlets' { Should -Match '^testtest' } + It 'Should throw if trying to rename entries created from input Stream' { + { Invoke-WebRequest $uri | Get-ZipEntry | Rename-ZipEntry -NewName { 'test' + $_.Name } } | + Should -Throw -ExceptionType ([System.NotSupportedException]) + } + It 'Produces output with -PassThru' { $zip | Get-ZipEntry -Type Archive | Rename-ZipEntry -NewName { $_.Name -replace 'test' } -PassThru | @@ -357,6 +380,7 @@ Describe 'ZipEntry Cmdlets' { BeforeAll { $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force $destination = New-Item (Join-Path $TestDrive -ChildPath 'ExtractTests') -ItemType Directory + $destination = $destination.FullName $structure = Get-Structure $content = 'hello world!' $content | New-ZipEntry $zip.FullName -EntryPath $structure @@ -368,6 +392,17 @@ Describe 'ZipEntry Cmdlets' { Should -Not -Throw } + It 'Can extract zip file entries created from input Stream' { + Invoke-WebRequest $uri | Get-ZipEntry -Type Archive -Include *.psd1 | + Expand-ZipEntry -Destination $destination -PassThru -OutVariable psd1 | + Should -BeOfType ([System.IO.FileInfo]) + + Get-Content $psd1.FullName -Raw | Invoke-Expression | + Should -BeOfType ([System.Collections.IDictionary]) + + $psd1.Delete() + } + It 'Should throw when -Destination is an invalid path' { { $zip | Get-ZipEntry | Expand-ZipEntry -Destination function: } | Should -Throw From e1b3c470325aa9df6e29162bda68b42e37f141a3 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 18:33:25 -0300 Subject: [PATCH 08/15] mistakes were made --- Get-ZipEntry.md | 239 +++++++++++++++++++++++ docs/en-US/Get-ZipEntry.md | 60 +++++- src/PSCompression/CommandWithPathBase.cs | 7 +- 3 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 Get-ZipEntry.md diff --git a/Get-ZipEntry.md b/Get-ZipEntry.md new file mode 100644 index 0000000..5d966b9 --- /dev/null +++ b/Get-ZipEntry.md @@ -0,0 +1,239 @@ +--- +external help file: PSCompression.dll-Help.xml +Module Name: PSCompression +online version: https://github.com/santisq/PSCompression +schema: 2.0.0 +--- + +# Get-ZipEntry + +## SYNOPSIS +Lists zip entries from one or more specified Zip Archives. + +## SYNTAX + +### Path (Default) +``` +Get-ZipEntry [-Type ] [-Include ] [-Exclude ] [-Path] + [-ProgressAction ] [] +``` + +### Stream +``` +Get-ZipEntry -Stream [-Type ] [-Include ] [-Exclude ] + [-ProgressAction ] [] +``` + +### LiteralPath +``` +Get-ZipEntry [-Type ] [-Include ] [-Exclude ] -LiteralPath + [-ProgressAction ] [] +``` + +## DESCRIPTION +The \`Get-ZipEntry\` cmdlet lists entries from specified Zip paths. +It has built-in functionalities to filter entries and is the main entry point for the \`*-ZipEntry\` cmdlets in this module. + +## EXAMPLES + +### Example 1: List entries for a specified Zip file path +``` +PS ..\pwsh> Get-ZipEntry path\to\myZip.zip +``` + +### Example 2: List entries from all Zip files in the current directory +``` +PS ..\pwsh> Get-ZipEntry *.zip +``` + +The \`-Path\` parameter supports wildcards. + +### Example 3: List all `Archive` entries from a Zip file +``` +PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Type Archive +``` + +The \`-Type\` parameter supports filtering by \`Archive\` or \`Directory\`. + +### Example 4: Filtering entries with `-Include` and `-Exclude` parameters +``` +PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-us* + + Directory: /PSCompression/docs/en-US/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Directory 2/22/2024 1:19 PM 0.00 B 0.00 B en-US +Archive 2/22/2024 1:19 PM 2.08 KB 6.98 KB Compress-GzipArchive.md +Archive 2/22/2024 1:19 PM 2.74 KB 8.60 KB Compress-ZipArchive.md +Archive 2/22/2024 1:19 PM 1.08 KB 2.67 KB ConvertFrom-GzipString.md +Archive 2/22/2024 1:19 PM 1.67 KB 4.63 KB ConvertTo-GzipString.md +Archive 2/22/2024 1:19 PM 1.74 KB 6.28 KB Expand-GzipArchive.md +Archive 2/22/2024 1:19 PM 1.23 KB 4.07 KB Expand-ZipEntry.md +Archive 2/22/2024 1:19 PM 1.53 KB 6.38 KB Get-ZipEntry.md +Archive 2/22/2024 1:19 PM 1.67 KB 5.06 KB Get-ZipEntryContent.md +Archive 2/22/2024 1:19 PM 2.20 KB 7.35 KB New-ZipEntry.md +Archive 2/22/2024 1:19 PM 961.00 B 2.62 KB PSCompression.md +Archive 2/22/2024 1:19 PM 1.14 KB 2.95 KB Remove-ZipEntry.md +Archive 2/22/2024 1:19 PM 741.00 B 2.16 KB Rename-ZipEntry.md +Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEntryContent.md + +PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-us* -Exclude *en-US/Compress*, *en-US/Remove* + + Directory: /PSCompression/docs/en-US/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Directory 2/22/2024 1:19 PM 0.00 B 0.00 B en-US +Archive 2/22/2024 1:19 PM 1.08 KB 2.67 KB ConvertFrom-GzipString.md +Archive 2/22/2024 1:19 PM 1.67 KB 4.63 KB ConvertTo-GzipString.md +Archive 2/22/2024 1:19 PM 1.74 KB 6.28 KB Expand-GzipArchive.md +Archive 2/22/2024 1:19 PM 1.23 KB 4.07 KB Expand-ZipEntry.md +Archive 2/22/2024 1:19 PM 1.53 KB 6.38 KB Get-ZipEntry.md +Archive 2/22/2024 1:19 PM 1.67 KB 5.06 KB Get-ZipEntryContent.md +Archive 2/22/2024 1:19 PM 2.20 KB 7.35 KB New-ZipEntry.md +Archive 2/22/2024 1:19 PM 961.00 B 2.62 KB PSCompression.md +Archive 2/22/2024 1:19 PM 741.00 B 2.16 KB Rename-ZipEntry.md +Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEntryContent.md +``` + +\> \[!NOTE\] \> \> - Inclusion and Exclusion patterns are applied to the entries relative path. +\> - Exclusions are applied after the inclusions. + +## PARAMETERS + +### -Type +Lists entries of a specified type, \`Archive\` or \`Directory\`. + +```yaml +Type: ZipEntryType +Parameter Sets: (All) +Aliases: +Accepted values: Directory, Archive + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Exclude +Specifies an array of one or more string patterns to be matched as the cmdlet lists entries. +Any matching item is excluded from the output. +Wildcard characters are accepted. + +\> \[!NOTE\] \> Inclusion and Exclusion patterns are applied to the entries relative path. +Exclusions are applied after the inclusions. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: True +``` + +### -Include +Specifies an array of one or more string patterns to be matched as the cmdlet lists entries. +Any matching item is included in the output. +Wildcard characters are accepted. + +\> \[!NOTE\] \> Inclusion and Exclusion patterns are applied to the entries relative path. +Exclusions are applied after the inclusions. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: True +``` + +### -LiteralPath +Specifies a path to one or more Zip compressed files. +Note that the value is used exactly as it's typed. +No characters are interpreted as wildcards. + +```yaml +Type: String[] +Parameter Sets: LiteralPath +Aliases: PSPath + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Path +Specifies a path to one or more Zip compressed files. +Wildcards are accepted. + +```yaml +Type: String[] +Parameter Sets: Path +Aliases: + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: True +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Stream +{{ Fill Stream Description }} + +```yaml +Type: Stream +Parameter Sets: Stream +Aliases: RawContentStream + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### String +You can pipe paths to this cmdlet. +Output from \`Get-ChildItem\` or \`Get-Item\` can be piped to this cmdlet. + +## OUTPUTS + +### ZipEntryDirectory +### ZipEntryFile +## NOTES + +## RELATED LINKS diff --git a/docs/en-US/Get-ZipEntry.md b/docs/en-US/Get-ZipEntry.md index 36154cf..7011e2d 100644 --- a/docs/en-US/Get-ZipEntry.md +++ b/docs/en-US/Get-ZipEntry.md @@ -35,19 +35,30 @@ Get-ZipEntry [] ``` +### Stream + +```powershell +Get-ZipEntry + -Stream + [-Type ] + [-Include ] + [-Exclude ] + [] +``` + ## DESCRIPTION -The `Get-ZipEntry` cmdlet lists entries from specified Zip paths. It has built-in functionalities to filter entries and is the main entry point for the `*-ZipEntry` cmdlets in this module. +The `Get-ZipEntry` cmdlet is the main entry point for the `*-ZipEntry` cmdlets in this module. It can list zip archive entries from a specified path or stream. ## EXAMPLES -### Example 1: List entries for a specified Zip file path +### Example 1: List entries for a specified file path ```powershell PS ..\pwsh> Get-ZipEntry path\to\myZip.zip ``` -### Example 2: List entries from all Zip files in the current directory +### Example 2: List entries from all files with `.zip` extension in the current directory ```powershell PS ..\pwsh> Get-ZipEntry *.zip @@ -111,6 +122,49 @@ Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEnt > - Inclusion and Exclusion patterns are applied to the entries relative path. > - Exclusions are applied after the inclusions. +### Example 5: List entries from an input stream + +```powershell +PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression +PS ..\pwsh> $package | Get-ZipEntry + + Directory: / + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Archive 11/6/2024 10:29 PM 227.00 B 785.00 B [Content_Types].xml +Archive 11/6/2024 10:27 PM 516.00 B 2.50 KB PSCompression.Format.ps1xml +Archive 11/6/2024 10:29 PM 598.00 B 1.58 KB PSCompression.nuspec +Archive 11/6/2024 10:27 PM 1.66 KB 5.45 KB PSCompression.psd1 + + Directory: /_rels/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Archive 11/6/2024 10:29 PM 276.00 B 507.00 B .rels + + Directory: /bin/netstandard2.0/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Archive 11/6/2024 10:28 PM 996.00 B 3.12 KB PSCompression.deps.json +Archive 11/6/2024 10:28 PM 28.73 KB 66.00 KB PSCompression.dll +Archive 11/6/2024 10:28 PM 14.75 KB 29.39 KB PSCompression.pdb + + Directory: /en-US/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Archive 11/6/2024 10:28 PM 8.33 KB 106.86 KB PSCompression-help.xml +Archive 11/6/2024 10:28 PM 9.19 KB 103.84 KB PSCompression.dll-Help.xml + + Directory: /package/services/metadata/core-properties/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Archive 11/6/2024 10:29 PM 635.00 B 1.55 KB 3212d87de09c4241a06e0166a08c3b13.psmdcp +``` + ## PARAMETERS ### -Type diff --git a/src/PSCompression/CommandWithPathBase.cs b/src/PSCompression/CommandWithPathBase.cs index e0da45e..5d972b5 100644 --- a/src/PSCompression/CommandWithPathBase.cs +++ b/src/PSCompression/CommandWithPathBase.cs @@ -13,7 +13,10 @@ public abstract class CommandWithPathBase : PSCmdlet { protected string[] _paths = []; - protected bool IsLiteral { get => LiteralPath is not null; } + protected bool IsLiteral + { + get => MyInvocation.BoundParameters.ContainsKey("LiteralPath"); + } [Parameter( ParameterSetName = "Path", @@ -73,7 +76,7 @@ protected IEnumerable EnumerateResolvedPaths() foreach (string resolvedPath in resolvedPaths) { - if (!provider.Validate(path, throwOnInvalidProvider: false, this)) + if (provider.Validate(path, throwOnInvalidProvider: false, this)) { yield return resolvedPath; } From c2e1b56d20e7b66e71fa993f0494de211968a174 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 18:52:36 -0300 Subject: [PATCH 09/15] mistakes were made --- src/PSCompression/CommandWithPathBase.cs | 4 ++-- src/PSCompression/Exceptions/ExceptionHelpers.cs | 2 +- tests/ZipEntryCmdlets.tests.ps1 | 10 ++++++++-- tests/ZipEntryFile.tests.ps1 | 8 +------- tools/requiredModules.psd1 | 1 + 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/PSCompression/CommandWithPathBase.cs b/src/PSCompression/CommandWithPathBase.cs index 5d972b5..a5bc887 100644 --- a/src/PSCompression/CommandWithPathBase.cs +++ b/src/PSCompression/CommandWithPathBase.cs @@ -55,7 +55,7 @@ protected IEnumerable EnumerateResolvedPaths() provider: out provider, drive: out _); - if (provider.Validate(path, throwOnInvalidProvider: false, this)) + if (provider.Validate(resolved, throwOnInvalidProvider: false, this)) { yield return resolved; } @@ -76,7 +76,7 @@ protected IEnumerable EnumerateResolvedPaths() foreach (string resolvedPath in resolvedPaths) { - if (provider.Validate(path, throwOnInvalidProvider: false, this)) + if (provider.Validate(resolvedPath, throwOnInvalidProvider: true, this)) { yield return resolvedPath; } diff --git a/src/PSCompression/Exceptions/ExceptionHelpers.cs b/src/PSCompression/Exceptions/ExceptionHelpers.cs index d528414..afdcb53 100644 --- a/src/PSCompression/Exceptions/ExceptionHelpers.cs +++ b/src/PSCompression/Exceptions/ExceptionHelpers.cs @@ -28,7 +28,7 @@ internal static ErrorRecord NotDirectoryPath(string path, string paramname) => internal static ErrorRecord ToInvalidProviderError(this ProviderInfo provider, string path) => new( - new ArgumentException( + new NotSupportedException( $"The resolved path '{path}' is not a FileSystem path but '{provider.Name}'."), "NotFileSystemPath", ErrorCategory.InvalidArgument, path); diff --git a/tests/ZipEntryCmdlets.tests.ps1 b/tests/ZipEntryCmdlets.tests.ps1 index c654e4d..48b3e78 100644 --- a/tests/ZipEntryCmdlets.tests.ps1 +++ b/tests/ZipEntryCmdlets.tests.ps1 @@ -121,6 +121,11 @@ Describe 'ZipEntry Cmdlets' { It 'Can list entries from a Stream' { Invoke-WebRequest $uri | Get-ZipEntry | Should -BeOfType ([PSCompression.ZipEntryBase]) + + Use-Object ($stream = $zip.OpenRead()) { + $stream | Get-ZipEntry | + Should -BeOfType ([PSCompression.ZipEntryBase]) + } } It 'Should throw when not targetting a FileSystem Provider Path' { @@ -128,11 +133,12 @@ Describe 'ZipEntry Cmdlets' { } It 'Should throw when the path is not a Zip' { - { $file | Get-ZipEntry } | Should -Throw + { Get-ZipEntry $file.FullName } | + Should -Throw -ExceptionType ([System.ArgumentException]) } It 'Should throw if the path is not a file' { - { $pwd | Get-ZipEntry } | Should -Throw + { Get-ZipEntry $pwd.FullName } | Should -Throw } It 'Can list zip file entries' { diff --git a/tests/ZipEntryFile.tests.ps1 b/tests/ZipEntryFile.tests.ps1 index de8732f..ff5ad4f 100644 --- a/tests/ZipEntryFile.tests.ps1 +++ b/tests/ZipEntryFile.tests.ps1 @@ -27,14 +27,8 @@ Describe 'ZipEntryFile Class' { } It 'Should Open the source zip' { - try { - $stream = ($zip | Get-ZipEntry).OpenRead() + Use-Object ($stream = ($zip | Get-ZipEntry).OpenRead()) { $stream | Should -BeOfType ([System.IO.Compression.ZipArchive]) } - finally { - if ($stream -is [System.IDisposable]) { - $stream.Dispose() - } - } } } diff --git a/tools/requiredModules.psd1 b/tools/requiredModules.psd1 index a486014..6cfe5a0 100644 --- a/tools/requiredModules.psd1 +++ b/tools/requiredModules.psd1 @@ -3,4 +3,5 @@ platyPS = '0.14.2' PSScriptAnalyzer = '1.22.0' Pester = '5.6.0' + UseObject = '1.0.0' } From da7333e9d4b479fbf43be88ebdd1ffc2fa93fff2 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 18:55:27 -0300 Subject: [PATCH 10/15] mistakes were made --- tools/requiredModules.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requiredModules.psd1 b/tools/requiredModules.psd1 index 6cfe5a0..8f5f32a 100644 --- a/tools/requiredModules.psd1 +++ b/tools/requiredModules.psd1 @@ -3,5 +3,5 @@ platyPS = '0.14.2' PSScriptAnalyzer = '1.22.0' Pester = '5.6.0' - UseObject = '1.0.0' + PSUsing = '1.0.0' } From e39c33d68cec9062a409c6b794a5940dba1bfcf2 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 19:55:49 -0300 Subject: [PATCH 11/15] mistakes were made --- Get-ZipEntry.md | 239 ------------------ docs/en-US/Get-ZipEntry.md | 16 ++ .../Commands/GetZipEntryCommand.cs | 29 ++- .../Exceptions/ExceptionHelpers.cs | 12 + tests/ZipEntryCmdlets.tests.ps1 | 8 +- 5 files changed, 55 insertions(+), 249 deletions(-) delete mode 100644 Get-ZipEntry.md diff --git a/Get-ZipEntry.md b/Get-ZipEntry.md deleted file mode 100644 index 5d966b9..0000000 --- a/Get-ZipEntry.md +++ /dev/null @@ -1,239 +0,0 @@ ---- -external help file: PSCompression.dll-Help.xml -Module Name: PSCompression -online version: https://github.com/santisq/PSCompression -schema: 2.0.0 ---- - -# Get-ZipEntry - -## SYNOPSIS -Lists zip entries from one or more specified Zip Archives. - -## SYNTAX - -### Path (Default) -``` -Get-ZipEntry [-Type ] [-Include ] [-Exclude ] [-Path] - [-ProgressAction ] [] -``` - -### Stream -``` -Get-ZipEntry -Stream [-Type ] [-Include ] [-Exclude ] - [-ProgressAction ] [] -``` - -### LiteralPath -``` -Get-ZipEntry [-Type ] [-Include ] [-Exclude ] -LiteralPath - [-ProgressAction ] [] -``` - -## DESCRIPTION -The \`Get-ZipEntry\` cmdlet lists entries from specified Zip paths. -It has built-in functionalities to filter entries and is the main entry point for the \`*-ZipEntry\` cmdlets in this module. - -## EXAMPLES - -### Example 1: List entries for a specified Zip file path -``` -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -``` - -### Example 2: List entries from all Zip files in the current directory -``` -PS ..\pwsh> Get-ZipEntry *.zip -``` - -The \`-Path\` parameter supports wildcards. - -### Example 3: List all `Archive` entries from a Zip file -``` -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Type Archive -``` - -The \`-Type\` parameter supports filtering by \`Archive\` or \`Directory\`. - -### Example 4: Filtering entries with `-Include` and `-Exclude` parameters -``` -PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-us* - - Directory: /PSCompression/docs/en-US/ - -Type LastWriteTime CompressedSize Size Name ----- ------------- -------------- ---- ---- -Directory 2/22/2024 1:19 PM 0.00 B 0.00 B en-US -Archive 2/22/2024 1:19 PM 2.08 KB 6.98 KB Compress-GzipArchive.md -Archive 2/22/2024 1:19 PM 2.74 KB 8.60 KB Compress-ZipArchive.md -Archive 2/22/2024 1:19 PM 1.08 KB 2.67 KB ConvertFrom-GzipString.md -Archive 2/22/2024 1:19 PM 1.67 KB 4.63 KB ConvertTo-GzipString.md -Archive 2/22/2024 1:19 PM 1.74 KB 6.28 KB Expand-GzipArchive.md -Archive 2/22/2024 1:19 PM 1.23 KB 4.07 KB Expand-ZipEntry.md -Archive 2/22/2024 1:19 PM 1.53 KB 6.38 KB Get-ZipEntry.md -Archive 2/22/2024 1:19 PM 1.67 KB 5.06 KB Get-ZipEntryContent.md -Archive 2/22/2024 1:19 PM 2.20 KB 7.35 KB New-ZipEntry.md -Archive 2/22/2024 1:19 PM 961.00 B 2.62 KB PSCompression.md -Archive 2/22/2024 1:19 PM 1.14 KB 2.95 KB Remove-ZipEntry.md -Archive 2/22/2024 1:19 PM 741.00 B 2.16 KB Rename-ZipEntry.md -Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEntryContent.md - -PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-us* -Exclude *en-US/Compress*, *en-US/Remove* - - Directory: /PSCompression/docs/en-US/ - -Type LastWriteTime CompressedSize Size Name ----- ------------- -------------- ---- ---- -Directory 2/22/2024 1:19 PM 0.00 B 0.00 B en-US -Archive 2/22/2024 1:19 PM 1.08 KB 2.67 KB ConvertFrom-GzipString.md -Archive 2/22/2024 1:19 PM 1.67 KB 4.63 KB ConvertTo-GzipString.md -Archive 2/22/2024 1:19 PM 1.74 KB 6.28 KB Expand-GzipArchive.md -Archive 2/22/2024 1:19 PM 1.23 KB 4.07 KB Expand-ZipEntry.md -Archive 2/22/2024 1:19 PM 1.53 KB 6.38 KB Get-ZipEntry.md -Archive 2/22/2024 1:19 PM 1.67 KB 5.06 KB Get-ZipEntryContent.md -Archive 2/22/2024 1:19 PM 2.20 KB 7.35 KB New-ZipEntry.md -Archive 2/22/2024 1:19 PM 961.00 B 2.62 KB PSCompression.md -Archive 2/22/2024 1:19 PM 741.00 B 2.16 KB Rename-ZipEntry.md -Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEntryContent.md -``` - -\> \[!NOTE\] \> \> - Inclusion and Exclusion patterns are applied to the entries relative path. -\> - Exclusions are applied after the inclusions. - -## PARAMETERS - -### -Type -Lists entries of a specified type, \`Archive\` or \`Directory\`. - -```yaml -Type: ZipEntryType -Parameter Sets: (All) -Aliases: -Accepted values: Directory, Archive - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -Exclude -Specifies an array of one or more string patterns to be matched as the cmdlet lists entries. -Any matching item is excluded from the output. -Wildcard characters are accepted. - -\> \[!NOTE\] \> Inclusion and Exclusion patterns are applied to the entries relative path. -Exclusions are applied after the inclusions. - -```yaml -Type: String[] -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: True -``` - -### -Include -Specifies an array of one or more string patterns to be matched as the cmdlet lists entries. -Any matching item is included in the output. -Wildcard characters are accepted. - -\> \[!NOTE\] \> Inclusion and Exclusion patterns are applied to the entries relative path. -Exclusions are applied after the inclusions. - -```yaml -Type: String[] -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: True -``` - -### -LiteralPath -Specifies a path to one or more Zip compressed files. -Note that the value is used exactly as it's typed. -No characters are interpreted as wildcards. - -```yaml -Type: String[] -Parameter Sets: LiteralPath -Aliases: PSPath - -Required: True -Position: Named -Default value: None -Accept pipeline input: True (ByPropertyName) -Accept wildcard characters: False -``` - -### -Path -Specifies a path to one or more Zip compressed files. -Wildcards are accepted. - -```yaml -Type: String[] -Parameter Sets: Path -Aliases: - -Required: True -Position: 0 -Default value: None -Accept pipeline input: True (ByValue) -Accept wildcard characters: True -``` - -### -ProgressAction -{{ Fill ProgressAction Description }} - -```yaml -Type: ActionPreference -Parameter Sets: (All) -Aliases: proga - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -Stream -{{ Fill Stream Description }} - -```yaml -Type: Stream -Parameter Sets: Stream -Aliases: RawContentStream - -Required: True -Position: Named -Default value: None -Accept pipeline input: True (ByPropertyName, ByValue) -Accept wildcard characters: False -``` - -### CommonParameters -This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). - -## INPUTS - -### String -You can pipe paths to this cmdlet. -Output from \`Get-ChildItem\` or \`Get-Item\` can be piped to this cmdlet. - -## OUTPUTS - -### ZipEntryDirectory -### ZipEntryFile -## NOTES - -## RELATED LINKS diff --git a/docs/en-US/Get-ZipEntry.md b/docs/en-US/Get-ZipEntry.md index 7011e2d..e8144dc 100644 --- a/docs/en-US/Get-ZipEntry.md +++ b/docs/en-US/Get-ZipEntry.md @@ -256,6 +256,22 @@ Accept pipeline input: True (ByValue) Accept wildcard characters: True ``` +### -Stream + +{{ Fill Stream Description }} + +```yaml +Type: Stream +Parameter Sets: Stream +Aliases: RawContentStream + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/src/PSCompression/Commands/GetZipEntryCommand.cs b/src/PSCompression/Commands/GetZipEntryCommand.cs index ef0812a..13c3028 100644 --- a/src/PSCompression/Commands/GetZipEntryCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryCommand.cs @@ -69,6 +69,7 @@ protected override void BeginProcessing() protected override void ProcessRecord() { + IEnumerable entries; if (Stream is not null) { ZipEntryBase CreateFromStream(ZipArchiveEntry entry, bool isDirectory) => @@ -76,11 +77,23 @@ ZipEntryBase CreateFromStream(ZipArchiveEntry entry, bool isDirectory) => ? new ZipEntryDirectory(entry, Stream) : new ZipEntryFile(entry, Stream); - using ZipArchive zip = new(Stream, ZipArchiveMode.Read, true); - WriteObject( - GetEntries(zip, CreateFromStream), - enumerateCollection: true); - return; + try + { + using (ZipArchive zip = new(Stream, ZipArchiveMode.Read, true)) + { + entries = GetEntries(zip, CreateFromStream); + } + WriteObject(entries, enumerateCollection: true); + return; + } + catch (InvalidDataException exception) + { + ThrowTerminatingError(exception.ToInvalidZipArchive()); + } + catch (Exception exception) + { + WriteError(exception.ToOpenError("InputStream")); + } } foreach (string path in EnumerateResolvedPaths()) @@ -102,14 +115,16 @@ ZipEntryBase CreateFromFile(ZipArchiveEntry entry, bool isDirectory) => try { - IEnumerable entries; using (ZipArchive zip = ZipFile.OpenRead(path)) { entries = GetEntries(zip, CreateFromFile); } - WriteObject(entries, enumerateCollection: true); } + catch (InvalidDataException exception) + { + ThrowTerminatingError(exception.ToInvalidZipArchive()); + } catch (Exception exception) { WriteError(exception.ToOpenError(path)); diff --git a/src/PSCompression/Exceptions/ExceptionHelpers.cs b/src/PSCompression/Exceptions/ExceptionHelpers.cs index afdcb53..4e4a57a 100644 --- a/src/PSCompression/Exceptions/ExceptionHelpers.cs +++ b/src/PSCompression/Exceptions/ExceptionHelpers.cs @@ -62,6 +62,18 @@ internal static ErrorRecord ToEntryNotFoundError(this EntryNotFoundException exc internal static ErrorRecord ToEnumerationError(this Exception exception, object item) => new(exception, "EnumerationError", ErrorCategory.ReadError, item); + internal static ErrorRecord ToInvalidZipArchive(this InvalidDataException exception) => + new( + new InvalidDataException( + "Specified path or stream is not a valid zip archive, " + + "might be compressed using an unsupported method, " + + "or could be corrupted.", + exception), + "InvalidZipArchive", + ErrorCategory.InvalidData, + null); + + internal static void ThrowIfNotFound( this ZipArchive zip, string path, diff --git a/tests/ZipEntryCmdlets.tests.ps1 b/tests/ZipEntryCmdlets.tests.ps1 index 48b3e78..370a2a7 100644 --- a/tests/ZipEntryCmdlets.tests.ps1 +++ b/tests/ZipEntryCmdlets.tests.ps1 @@ -129,16 +129,18 @@ Describe 'ZipEntry Cmdlets' { } It 'Should throw when not targetting a FileSystem Provider Path' { - { Get-ZipEntry function:\* } | Should -Throw + { Get-ZipEntry function:\* } | + Should -Throw -ExceptionType ([System.NotSupportedException]) } It 'Should throw when the path is not a Zip' { { Get-ZipEntry $file.FullName } | - Should -Throw -ExceptionType ([System.ArgumentException]) + Should -Throw -ExceptionType ([System.IO.InvalidDataException]) } It 'Should throw if the path is not a file' { - { Get-ZipEntry $pwd.FullName } | Should -Throw + { Get-ZipEntry $pwd.FullName } | + Should -Throw -ExceptionType ([System.ArgumentException]) } It 'Can list zip file entries' { From 9e16b1868ecf4e4d5b4143fed38d464ae5f66cb4 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 9 Jan 2025 21:23:59 -0300 Subject: [PATCH 12/15] mistakes were made --- tests/ZipEntryCmdlets.tests.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ZipEntryCmdlets.tests.ps1 b/tests/ZipEntryCmdlets.tests.ps1 index 370a2a7..eda2a5d 100644 --- a/tests/ZipEntryCmdlets.tests.ps1 +++ b/tests/ZipEntryCmdlets.tests.ps1 @@ -21,7 +21,7 @@ Describe 'ZipEntry Cmdlets' { } It 'Should throw if -Destination is a Directory' { - { New-ZipEntry -Destination $pwd.Path -EntryPath bar } | + { New-ZipEntry -Destination $TestDrive -EntryPath bar } | Should -Throw } @@ -31,7 +31,7 @@ Describe 'ZipEntry Cmdlets' { } It 'Should throw if -Source is a Directory' { - { New-ZipEntry -Destination $zip.FullName -EntryPath baz -SourcePath $pwd.FullName } | + { New-ZipEntry -Destination $zip.FullName -EntryPath baz -SourcePath $TestDrive } | Should -Throw } @@ -139,7 +139,7 @@ Describe 'ZipEntry Cmdlets' { } It 'Should throw if the path is not a file' { - { Get-ZipEntry $pwd.FullName } | + { Get-ZipEntry $TestDrive } | Should -Throw -ExceptionType ([System.ArgumentException]) } From e87416df7c32c1d0aa9ce21a418b6beb03df56f4 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 10 Jan 2025 11:09:53 -0300 Subject: [PATCH 13/15] more tests --- tests/ZipEntryCmdlets.tests.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/ZipEntryCmdlets.tests.ps1 b/tests/ZipEntryCmdlets.tests.ps1 index eda2a5d..c5f4bcf 100644 --- a/tests/ZipEntryCmdlets.tests.ps1 +++ b/tests/ZipEntryCmdlets.tests.ps1 @@ -138,6 +138,14 @@ Describe 'ZipEntry Cmdlets' { Should -Throw -ExceptionType ([System.IO.InvalidDataException]) } + It 'Should throw when a Stream is not a Zip' { + { + Use-Object ($stream = $file.OpenRead()) { + Get-ZipEntry $stream + } + } | Should -Throw -ExceptionType ([System.IO.InvalidDataException]) + } + It 'Should throw if the path is not a file' { { Get-ZipEntry $TestDrive } | Should -Throw -ExceptionType ([System.ArgumentException]) From a3551f45b251939c5693fb26eb7895bcab16b85c Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 10 Jan 2025 15:35:46 -0300 Subject: [PATCH 14/15] fuckin coverlet shit --- .../Commands/CompressZipArchiveCommand.cs | 14 ++++++-------- src/PSCompression/Commands/GetZipEntryCommand.cs | 7 +++---- tests/ZipEntryCmdlets.tests.ps1 | 3 +++ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/PSCompression/Commands/CompressZipArchiveCommand.cs b/src/PSCompression/Commands/CompressZipArchiveCommand.cs index 8e20254..ec6116a 100644 --- a/src/PSCompression/Commands/CompressZipArchiveCommand.cs +++ b/src/PSCompression/Commands/CompressZipArchiveCommand.cs @@ -14,8 +14,6 @@ namespace PSCompression.Commands; [Alias("ziparchive")] public sealed class CompressZipArchiveCommand : CommandWithPathBase, IDisposable { - private const FileShare s_sharemode = FileShare.ReadWrite | FileShare.Delete; - private ZipArchive? _zip; private FileStream? _destination; @@ -83,14 +81,14 @@ protected override void BeginProcessing() ThrowTerminatingError(exception.ToStreamOpenError(Destination)); } - const WildcardOptions wpoptions = WildcardOptions.Compiled - | WildcardOptions.CultureInvariant - | WildcardOptions.IgnoreCase; - if (Exclude is not null) { + const WildcardOptions options = WildcardOptions.Compiled + | WildcardOptions.CultureInvariant + | WildcardOptions.IgnoreCase; + _excludePatterns = Exclude - .Select(e => new WildcardPattern(e, wpoptions)) + .Select(pattern => new WildcardPattern(pattern, options)) .ToArray(); } } @@ -208,7 +206,7 @@ private static FileStream Open(FileInfo file) => file.Open( mode: FileMode.Open, access: FileAccess.Read, - share: s_sharemode); + share: FileShare.ReadWrite | FileShare.Delete); private void UpdateEntry( FileInfo file, diff --git a/src/PSCompression/Commands/GetZipEntryCommand.cs b/src/PSCompression/Commands/GetZipEntryCommand.cs index 13c3028..c9a433e 100644 --- a/src/PSCompression/Commands/GetZipEntryCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryCommand.cs @@ -47,22 +47,21 @@ protected override void BeginProcessing() return; } - const WildcardOptions wpoptions = - WildcardOptions.Compiled + const WildcardOptions options = WildcardOptions.Compiled | WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; if (Exclude is not null) { _excludePatterns = Exclude - .Select(e => new WildcardPattern(e, wpoptions)) + .Select(e => new WildcardPattern(e, options)) .ToArray(); } if (Include is not null) { _includePatterns = Include - .Select(e => new WildcardPattern(e, wpoptions)) + .Select(e => new WildcardPattern(e, options)) .ToArray(); } } diff --git a/tests/ZipEntryCmdlets.tests.ps1 b/tests/ZipEntryCmdlets.tests.ps1 index c5f4bcf..820e53e 100644 --- a/tests/ZipEntryCmdlets.tests.ps1 +++ b/tests/ZipEntryCmdlets.tests.ps1 @@ -131,6 +131,9 @@ Describe 'ZipEntry Cmdlets' { It 'Should throw when not targetting a FileSystem Provider Path' { { Get-ZipEntry function:\* } | Should -Throw -ExceptionType ([System.NotSupportedException]) + + { Get-ZipEntry -LiteralPath function:\ } | + Should -Throw -ExceptionType ([System.NotSupportedException]) } It 'Should throw when the path is not a Zip' { From a450b67ec374cf985950de5ca26e8db0c3d4c1ee Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 10 Jan 2025 17:51:34 -0300 Subject: [PATCH 15/15] ready to merge --- CHANGELOG.md | 16 ++++++-- docs/en-US/Expand-ZipEntry.md | 26 ++++++++++++ docs/en-US/Get-ZipEntry.md | 28 ++++++++----- docs/en-US/Get-ZipEntryContent.md | 25 ++++++++++++ .../Commands/GetZipEntryCommand.cs | 12 +++--- src/PSCompression/ZipEntryBase.cs | 15 ++++++- src/PSCompression/ZipEntryFile.cs | 8 ---- tests/ZipEntryBase.tests.ps1 | 40 +++++++++++++++++++ tests/ZipEntryCmdlets.tests.ps1 | 24 +++++++++++ tools/requiredModules.psd1 | 6 +-- 10 files changed, 169 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 784464d..2f42ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,26 @@ -# 06/24/2024 +# CHANGELOG + +## 01/10/2025 + +- Code improvements. +- Instance methods `.OpenRead()` and `.OpenWrite()` moved from `ZipEntryFile` to `ZipEntryBase`. +- Adds support to list, read and extract zip archive entries from Stream. + +## 06/24/2024 - Update build process. -# 06/05/2024 +## 06/05/2024 - Update `ci.yml` to use `codecov-action@v4`. - Fixed parameter names in `Compress-ZipArchive` documentation. Thanks to @martincostello. - Fixed coverlet.console support for Linux runner tests. -# 02/26/2024 +## 02/26/2024 - Fixed a bug with `CompressionRatio` property showing always in InvariantCulture format. -# 02/25/2024 +## 02/25/2024 - `ZipEntryBase` Type: - Renamed Property `EntryName` to `Name`. diff --git a/docs/en-US/Expand-ZipEntry.md b/docs/en-US/Expand-ZipEntry.md index 520678e..96c2300 100644 --- a/docs/en-US/Expand-ZipEntry.md +++ b/docs/en-US/Expand-ZipEntry.md @@ -62,6 +62,32 @@ PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry -Pas By default this cmdlet produces no output. When `-PassThru` is used, this cmdlet outputs the `FileInfo` and `DirectoryInfo` instances representing the expanded entries. +### Example 6: Extract an entry from input Stream + +```powershell +PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression +PS ..\pwsh> $file = $package | Get-ZipEntry -Include *.psd1 | Expand-ZipEntry -PassThru -Force +PS ..\pwsh> Get-Content $file.FullName -Raw | Invoke-Expression + +Name Value +---- ----- +PowerShellVersion 5.1 +Description Zip and GZip utilities for PowerShell! +RootModule bin/netstandard2.0/PSCompression.dll +FormatsToProcess {PSCompression.Format.ps1xml} +VariablesToExport {} +PrivateData {[PSData, System.Collections.Hashtable]} +CmdletsToExport {Get-ZipEntry, Get-ZipEntryContent, Set-ZipEntryContent, Remove-ZipEntry…} +ModuleVersion 2.0.10 +Author Santiago Squarzon +CompanyName Unknown +GUID c63aa90e-ae64-4ae1-b1c8-456e0d13967e +FunctionsToExport {} +RequiredAssemblies {System.IO.Compression, System.IO.Compression.FileSystem} +Copyright (c) Santiago Squarzon. All rights reserved. +AliasesToExport {gziptofile, gzipfromfile, gziptostring, gzipfromstring…} +``` + ## PARAMETERS ### -Destination diff --git a/docs/en-US/Get-ZipEntry.md b/docs/en-US/Get-ZipEntry.md index e8144dc..6f1e0fa 100644 --- a/docs/en-US/Get-ZipEntry.md +++ b/docs/en-US/Get-ZipEntry.md @@ -9,7 +9,7 @@ schema: 2.0.0 ## SYNOPSIS -Lists zip entries from one or more specified Zip Archives. +Lists zip archive entries from specified path or input stream. ## SYNTAX @@ -39,7 +39,7 @@ Get-ZipEntry ```powershell Get-ZipEntry - -Stream + -InputStream [-Type ] [-Include ] [-Exclude ] @@ -48,7 +48,7 @@ Get-ZipEntry ## DESCRIPTION -The `Get-ZipEntry` cmdlet is the main entry point for the `*-ZipEntry` cmdlets in this module. It can list zip archive entries from a specified path or stream. +The `Get-ZipEntry` cmdlet is the main entry point for the `*-ZipEntry` cmdlets in this module. It can list zip archive entries from a specified path or input stream. ## EXAMPLES @@ -72,7 +72,8 @@ The `-Path` parameter supports wildcards. PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Type Archive ``` -The `-Type` parameter supports filtering by `Archive` or `Directory`. +> [!TIP] +> The `-Type` parameter supports filtering by `Archive` or `Directory`. ### Example 4: Filtering entries with `-Include` and `-Exclude` parameters @@ -122,7 +123,7 @@ Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEnt > - Inclusion and Exclusion patterns are applied to the entries relative path. > - Exclusions are applied after the inclusions. -### Example 5: List entries from an input stream +### Example 5: List entries from an input Stream ```powershell PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression @@ -226,7 +227,7 @@ Accept wildcard characters: True ### -LiteralPath -Specifies a path to one or more Zip compressed files. Note that the value is used exactly as it's typed. No characters are interpreted as wildcards. +Specifies a path to one or more zip archives. Note that the value is used exactly as it's typed. No characters are interpreted as wildcards. ```yaml Type: String[] @@ -242,7 +243,7 @@ Accept wildcard characters: False ### -Path -Specifies a path to one or more Zip compressed files. Wildcards are accepted. +Specifies a path to one or more zip archives. Wildcards are accepted. ```yaml Type: String[] @@ -256,9 +257,12 @@ Accept pipeline input: True (ByValue) Accept wildcard characters: True ``` -### -Stream +### -InputStream -{{ Fill Stream Description }} +Specifies an input stream. + +> [!TIP] +> Output from `Invoke-WebRequest` is bound to this paremeter automatically. ```yaml Type: Stream @@ -280,7 +284,11 @@ This cmdlet supports the common parameters. For more information, see [about_Com ### String -You can pipe paths to this cmdlet. Output from `Get-ChildItem` or `Get-Item` can be piped to this cmdlet. +You can pipe a string that contains a paths to this cmdlet. Output from `Get-ChildItem` or `Get-Item` can be piped to this cmdlet. + +### Stream + +You can pipe a Stream to this cmdlet. Output from `Invoke-WebRequest` can be piped to this cmdlet. ## OUTPUTS diff --git a/docs/en-US/Get-ZipEntryContent.md b/docs/en-US/Get-ZipEntryContent.md index adb271e..53cb35c 100644 --- a/docs/en-US/Get-ZipEntryContent.md +++ b/docs/en-US/Get-ZipEntryContent.md @@ -103,6 +103,31 @@ PS ..pwsh\> $bytes[1].Length When the `-Raw` and `-AsByteStream` switches are used together the cmdlet outputs `byte[]` as single objects for each zip entry. +### Example 5: Get content from input Stream + +```powershell +PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression +PS ..\pwsh> $package | Get-ZipEntry -Include *.psd1 | Get-ZipEntryContent -Raw | Invoke-Expression + +Name Value +---- ----- +PowerShellVersion 5.1 +Description Zip and GZip utilities for PowerShell! +RootModule bin/netstandard2.0/PSCompression.dll +FormatsToProcess {PSCompression.Format.ps1xml} +VariablesToExport {} +PrivateData {[PSData, System.Collections.Hashtable]} +CmdletsToExport {Get-ZipEntry, Get-ZipEntryContent, Set-ZipEntryContent, Remove-ZipEntry…} +ModuleVersion 2.0.10 +Author Santiago Squarzon +CompanyName Unknown +GUID c63aa90e-ae64-4ae1-b1c8-456e0d13967e +FunctionsToExport {} +RequiredAssemblies {System.IO.Compression, System.IO.Compression.FileSystem} +Copyright (c) Santiago Squarzon. All rights reserved. +AliasesToExport {gziptofile, gzipfromfile, gziptostring, gzipfromstring…} +``` + ## PARAMETERS ### -BufferSize diff --git a/src/PSCompression/Commands/GetZipEntryCommand.cs b/src/PSCompression/Commands/GetZipEntryCommand.cs index c9a433e..43502a4 100644 --- a/src/PSCompression/Commands/GetZipEntryCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryCommand.cs @@ -21,7 +21,7 @@ public sealed class GetZipEntryCommand : CommandWithPathBase ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [Alias("RawContentStream")] - public Stream? Stream { get; set; } + public Stream? InputStream { get; set; } private readonly List _output = []; @@ -69,19 +69,20 @@ protected override void BeginProcessing() protected override void ProcessRecord() { IEnumerable entries; - if (Stream is not null) + if (InputStream is not null) { ZipEntryBase CreateFromStream(ZipArchiveEntry entry, bool isDirectory) => isDirectory - ? new ZipEntryDirectory(entry, Stream) - : new ZipEntryFile(entry, Stream); + ? new ZipEntryDirectory(entry, InputStream) + : new ZipEntryFile(entry, InputStream); try { - using (ZipArchive zip = new(Stream, ZipArchiveMode.Read, true)) + using (ZipArchive zip = new(InputStream, ZipArchiveMode.Read, true)) { entries = GetEntries(zip, CreateFromStream); } + WriteObject(entries, enumerateCollection: true); return; } @@ -118,6 +119,7 @@ ZipEntryBase CreateFromFile(ZipArchiveEntry entry, bool isDirectory) => { entries = GetEntries(zip, CreateFromFile); } + WriteObject(entries, enumerateCollection: true); } catch (InvalidDataException exception) diff --git a/src/PSCompression/ZipEntryBase.cs b/src/PSCompression/ZipEntryBase.cs index ab8ed40..06072b6 100644 --- a/src/PSCompression/ZipEntryBase.cs +++ b/src/PSCompression/ZipEntryBase.cs @@ -36,6 +36,16 @@ protected ZipEntryBase(ZipArchiveEntry entry, Stream? stream) _stream = stream; } + public ZipArchive OpenRead() => _stream is null + ? ZipFile.OpenRead(Source) + : new ZipArchive(_stream); + + public ZipArchive OpenWrite() + { + this.ThrowIfFromStream(); + return ZipFile.Open(Source, ZipArchiveMode.Update); + } + public void Remove() { this.ThrowIfFromStream(); @@ -99,7 +109,10 @@ _stream is null public FileSystemInfo ExtractTo(string destination, bool overwrite) { - using ZipArchive zip = ZipFile.OpenRead(Source); + using ZipArchive zip = _stream is null + ? ZipFile.OpenRead(Source) + : new ZipArchive(_stream); + (string path, bool isArchive) = this.ExtractTo(zip, destination, overwrite); if (isArchive) diff --git a/src/PSCompression/ZipEntryFile.cs b/src/PSCompression/ZipEntryFile.cs index c093cbb..3c76543 100644 --- a/src/PSCompression/ZipEntryFile.cs +++ b/src/PSCompression/ZipEntryFile.cs @@ -41,14 +41,6 @@ private static string GetRatio(long size, long compressedSize) return string.Format("{0:F2}%", 100 - (compressedRatio * 100)); } - public ZipArchive OpenRead() => ZipFile.OpenRead(Source); - - public ZipArchive OpenWrite() - { - this.ThrowIfFromStream(); - return ZipFile.Open(Source, ZipArchiveMode.Update); - } - internal Stream Open(ZipArchive zip) { zip.ThrowIfNotFound( diff --git a/tests/ZipEntryBase.tests.ps1 b/tests/ZipEntryBase.tests.ps1 index c3093b9..43afcad 100644 --- a/tests/ZipEntryBase.tests.ps1 +++ b/tests/ZipEntryBase.tests.ps1 @@ -23,6 +23,12 @@ Describe 'ZipEntryBase Class' { Should -BeOfType ([System.IO.FileInfo]) } + It 'Can extract a file from entries created from input Stream' { + Use-Object ($stream = $zip.OpenRead()) { + ($stream | Get-ZipEntry -Type Archive).ExtractTo($TestDrive, $true) + } | Should -BeOfType ([System.IO.FileInfo]) + } + It 'Can create a new folder in the destination path when extracting' { $entry = $zip | Get-ZipEntry -Type Archive $file = $entry.ExtractTo( @@ -57,4 +63,38 @@ Describe 'ZipEntryBase Class' { $zip | Get-ZipEntry | Should -BeNullOrEmpty } + + It 'Should throw if Remove() is used on entries created from input Stream' { + 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt + + { + Use-Object ($stream = $zip.OpenRead()) { + $stream | Get-ZipEntry -Type Archive | ForEach-Object Remove + } + } | Should -Throw + } + + It 'Opens a ZipArchive on OpenRead() and OpenWrite()' { + Use-Object ($archive = ($zip | Get-ZipEntry).OpenRead()) { + $archive | Should -BeOfType ([System.IO.Compression.ZipArchive]) + } + + Use-Object ($stream = $zip.OpenRead()) { + Use-Object ($archive = ($stream | Get-ZipEntry).OpenRead()) { + $archive | Should -BeOfType ([System.IO.Compression.ZipArchive]) + } + } + + Use-Object ($archive = ($zip | Get-ZipEntry).OpenWrite()) { + $archive | Should -BeOfType ([System.IO.Compression.ZipArchive]) + } + } + + It 'Should throw if calling OpenWrite() on entries created from input Stream' { + Use-Object ($stream = $zip.OpenRead()) { + { + Use-Object ($stream | Get-ZipEntry).OpenWrite() { } + } | Should -Throw + } + } } diff --git a/tests/ZipEntryCmdlets.tests.ps1 b/tests/ZipEntryCmdlets.tests.ps1 index 820e53e..e414608 100644 --- a/tests/ZipEntryCmdlets.tests.ps1 +++ b/tests/ZipEntryCmdlets.tests.ps1 @@ -149,6 +149,12 @@ Describe 'ZipEntry Cmdlets' { } | Should -Throw -ExceptionType ([System.IO.InvalidDataException]) } + It 'Should throw when a Stream is Disposed' { + { + (Use-Object ($stream = (Invoke-WebRequest $uri).RawContentStream) { $stream }) | Get-ZipEntry + } | Should -Throw -ExceptionType ([System.ObjectDisposedException]) + } + It 'Should throw if the path is not a file' { { Get-ZipEntry $TestDrive } | Should -Throw -ExceptionType ([System.ArgumentException]) @@ -190,6 +196,15 @@ Describe 'ZipEntry Cmdlets' { Should -BeOfType ([System.Collections.IDictionary]) } + It 'Should throw when a Stream is Diposed' { + { + $entry = Use-Object ($stream = (Invoke-WebRequest $uri).RawContentStream) { + $stream | Get-ZipEntry -Type Archive -Include *.psd1 + } + $entry | Get-ZipEntryContent -Raw + } | Should -Throw -ExceptionType ([System.ObjectDisposedException]) + } + It 'Should not throw when an instance wrapped in PSObject is passed as Encoding argument' { $enc = Write-Output utf8 { $zip | Get-ZipEntry -Type Archive | Get-ZipEntryContent -Encoding $enc } | @@ -422,6 +437,15 @@ Describe 'ZipEntry Cmdlets' { $psd1.Delete() } + It 'Should throw when a Stream is Diposed' { + { + $entry = Use-Object ($stream = (Invoke-WebRequest $uri).RawContentStream) { + $stream | Get-ZipEntry -Type Archive -Include *.psd1 + } + $entry | Expand-ZipEntry -Destination $destination -Force + } | Should -Throw -ExceptionType ([System.ObjectDisposedException]) + } + It 'Should throw when -Destination is an invalid path' { { $zip | Get-ZipEntry | Expand-ZipEntry -Destination function: } | Should -Throw diff --git a/tools/requiredModules.psd1 b/tools/requiredModules.psd1 index 8f5f32a..b49d079 100644 --- a/tools/requiredModules.psd1 +++ b/tools/requiredModules.psd1 @@ -1,7 +1,7 @@ @{ - InvokeBuild = '5.11.2' + InvokeBuild = '5.12.1' platyPS = '0.14.2' - PSScriptAnalyzer = '1.22.0' - Pester = '5.6.0' + PSScriptAnalyzer = '1.23.0' + Pester = '5.7.1' PSUsing = '1.0.0' }