Skip to content

Commit f7cdfd2

Browse files
committed
updated Compress-Archive cmdlet to resolve a path one at a time rather than collecting all paths first
1 parent f7c2af4 commit f7cdfd2

File tree

5 files changed

+321
-347
lines changed

5 files changed

+321
-347
lines changed

src/ArchiveMode.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Text;
7-
84
namespace Microsoft.PowerShell.Archive
95
{
106
internal enum ArchiveMode

src/CompressArchiveCommand.cs

Lines changed: 94 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
using System.Diagnostics.CodeAnalysis;
88
using System.IO;
99
using System.IO.Compression;
10-
using System.Linq;
1110
using System.Management.Automation;
11+
using System.Runtime.InteropServices;
1212

1313
using Microsoft.PowerShell.Archive.Localized;
1414

@@ -68,85 +68,105 @@ private enum ParameterSet
6868
[Parameter()]
6969
public ArchiveFormat? Format { get; set; } = null;
7070

71-
// Stores paths from -Path parameter
72-
private List<string>? _literalPaths;
71+
private readonly PathHelper _pathHelper;
7372

74-
// Stores paths from -LiteralPath parameter
75-
private List<string>? _nonliteralPaths;
73+
private bool _didCreateNewArchive;
7674

77-
private readonly PathHelper _pathHelper;
75+
// Stores paths
76+
private HashSet<string>? _paths;
7877

79-
private FileSystemInfo? _destinationPathInfo;
78+
// This is used so the cmdlet can show all nonexistent paths at once to the user
79+
private HashSet<string> _nonexistentPaths;
8080

81-
private bool _didCreateNewArchive;
81+
// Keeps track of duplicate paths so the cmdlet can show them all at once to the user
82+
private HashSet<string> _duplicatePaths;
83+
84+
// Keeps track of whether any source path is equal to the destination path
85+
// Since we are already checking for duplicates, only a bool is necessary and not a List or a HashSet
86+
// Only 1 path could be equal to the destination path after filtering for duplicates
87+
private bool _isSourcePathEqualToDestinationPath;
8288

8389
public CompressArchiveCommand()
8490
{
85-
_literalPaths = new List<string>();
86-
_nonliteralPaths = new List<string>();
8791
_pathHelper = new PathHelper(this);
8892
Messages.Culture = new System.Globalization.CultureInfo("en-US");
8993
_didCreateNewArchive = false;
90-
_destinationPathInfo = null;
94+
_paths = new HashSet<string>( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
95+
_nonexistentPaths = new HashSet<string>( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
96+
_duplicatePaths = new HashSet<string>( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
9197
}
9298

9399
protected override void BeginProcessing()
94100
{
95-
_destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath);
96-
DestinationPath = _destinationPathInfo.FullName;
101+
// This resolves the path to a fully qualified path and handles provider exceptions
102+
DestinationPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(DestinationPath);
97103
ValidateDestinationPath();
98104
}
99105

100106
protected override void ProcessRecord()
101107
{
102-
// Add each path from -Path or -LiteralPath to _nonliteralPaths or _literalPaths because they can get lost when the next item in the pipeline is sent
103108
if (ParameterSetName == nameof(ParameterSet.Path))
104109
{
105110
Debug.Assert(Path is not null);
106-
_nonliteralPaths?.AddRange(Path);
111+
foreach (var path in Path) {
112+
var resolvedPaths = _pathHelper.GetResolvedPathFromPSProviderPath(path, _nonexistentPaths);
113+
if (resolvedPaths is not null) {
114+
foreach (var resolvedPath in resolvedPaths) {
115+
// Add resolvedPath to _path
116+
AddPathToPaths(pathToAdd: resolvedPath);
117+
}
118+
}
119+
}
120+
107121
}
108122
else
109123
{
110124
Debug.Assert(LiteralPath is not null);
111-
_literalPaths?.AddRange(LiteralPath);
125+
foreach (var path in LiteralPath) {
126+
var unresolvedPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(path, _nonexistentPaths);
127+
if (unresolvedPath is not null) {
128+
// Add unresolvedPath to _path
129+
AddPathToPaths(pathToAdd: unresolvedPath);
130+
}
131+
}
112132
}
113133
}
114134

115135
protected override void EndProcessing()
116136
{
117-
Debug.Assert(_destinationPathInfo is not null);
118-
Debug.Assert(_literalPaths is not null);
119-
Debug.Assert(_nonliteralPaths is not null);
137+
// If there are non-existent paths, throw a terminating error
138+
if (_nonexistentPaths.Count > 0) {
139+
// Get a comma-seperated string containg the non-existent paths
140+
string commaSeperatedNonExistentPaths = string.Join(',', _nonexistentPaths);
141+
var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.InvalidPath, commaSeperatedNonExistentPaths);
142+
ThrowTerminatingError(errorRecord);
143+
}
120144

121-
// Get archive entries, validation is performed by PathHelper
122-
// _literalPaths should not be null at this stage, but if it is, prevent a NullReferenceException by doing the following
123-
List<ArchiveAddition> archiveAdditions = _pathHelper.GetArchiveAdditionsForPath(paths: _literalPaths.ToArray(), literalPath: true);
145+
// If there are duplicate paths, throw a terminating error
146+
if (_duplicatePaths.Count > 0) {
147+
// Get a comma-seperated string containg the non-existent paths
148+
string commaSeperatedDuplicatePaths = string.Join(',', _nonexistentPaths);
149+
var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.DuplicatePaths, commaSeperatedDuplicatePaths);
150+
ThrowTerminatingError(errorRecord);
151+
}
124152

125-
// Do the same as above for _nonliteralPaths
126-
List<ArchiveAddition>? nonliteralArchiveAdditions = _pathHelper.GetArchiveAdditionsForPath(paths: _nonliteralPaths.ToArray(), literalPath: false);
153+
// If a source path is the same as the destination path, throw a terminating error
154+
// We don't want to overwrite the file or directory that we want to add to the archive.
155+
if (_isSourcePathEqualToDestinationPath) {
156+
var errorCode = ParameterSetName == nameof(ParameterSet.Path) ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath;
157+
var errorRecord = ErrorMessages.GetErrorRecord(errorCode);
158+
ThrowTerminatingError(errorRecord);
159+
}
127160

128-
// Add nonliteralArchiveAdditions to archive additions, so we can keep track of one list only
129-
archiveAdditions.AddRange(nonliteralArchiveAdditions);
161+
// Get archive entries
162+
// If a path causes an exception (e.g., SecurityException), _pathHelper should handle it
163+
List<ArchiveAddition> archiveAdditions = _pathHelper.GetArchiveAdditions(_paths);
130164

131-
// Remove references to _sourcePaths, Path, and LiteralPath to free up memory
165+
// Remove references to _paths, Path, and LiteralPath to free up memory
132166
// The user could have supplied a lot of paths, so we should do this
133167
Path = null;
134168
LiteralPath = null;
135-
_literalPaths = null;
136-
_nonliteralPaths = null;
137-
// Remove reference to nonliteralArchiveAdditions since we do not use it any more
138-
nonliteralArchiveAdditions = null;
139-
140-
// Throw a terminating error if there is a source path as same as DestinationPath.
141-
// We don't want to overwrite the file or directory that we want to add to the archive.
142-
var additionsWithSamePathAsDestination = archiveAdditions.Where(addition => PathHelper.ArePathsSame(addition.FileSystemInfo, _destinationPathInfo)).ToList();
143-
if (additionsWithSamePathAsDestination.Count > 0)
144-
{
145-
// Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath
146-
var errorCode = ParameterSetName == nameof(ParameterSet.Path) ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath;
147-
var errorRecord = ErrorMessages.GetErrorRecord(errorCode, errorItem: additionsWithSamePathAsDestination[0].FileSystemInfo.FullName);
148-
ThrowTerminatingError(errorRecord);
149-
}
169+
_paths = null;
150170

151171
// Warn the user if there are no items to add for some reason (e.g., no items matched the filter)
152172
if (archiveAdditions.Count == 0)
@@ -161,20 +181,18 @@ protected override void EndProcessing()
161181
IArchive? archive = null;
162182
try
163183
{
164-
if (ShouldProcess(target: _destinationPathInfo.FullName, action: Messages.Create))
184+
if (ShouldProcess(target: DestinationPath, action: Messages.Create))
165185
{
166186
// If the WriteMode is overwrite, delete the existing archive
167187
if (WriteMode == WriteMode.Overwrite)
168188
{
169189
DeleteDestinationPathIfExists();
170-
_destinationPathInfo = new FileInfo(_destinationPathInfo.FullName);
171190
}
172191

173192
// Create an archive -- this is where we will switch between different types of archives
174193
archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel);
175-
_didCreateNewArchive = archiveMode == ArchiveMode.Update;
194+
_didCreateNewArchive = archiveMode != ArchiveMode.Update;
176195
}
177-
178196

179197
long numberOfAdditions = archiveAdditions.Count;
180198
long numberOfAddedItems = 0;
@@ -212,7 +230,7 @@ protected override void EndProcessing()
212230
// If -PassThru is specified, write a System.IO.FileInfo object
213231
if (PassThru)
214232
{
215-
WriteObject(_destinationPathInfo);
233+
WriteObject(new FileInfo(DestinationPath));
216234
}
217235
}
218236

@@ -221,7 +239,7 @@ protected override void StopProcessing()
221239
// If a new output archive was created, delete it (this does not delete an archive if -WriteMode Update is specified)
222240
if (_didCreateNewArchive)
223241
{
224-
_destinationPathInfo?.Delete();
242+
DeleteDestinationPathIfExists();
225243
}
226244
}
227245

@@ -230,13 +248,12 @@ protected override void StopProcessing()
230248
/// </summary>
231249
private void ValidateDestinationPath()
232250
{
233-
Debug.Assert(_destinationPathInfo is not null);
234251
ErrorCode? errorCode = null;
235252

236-
if (_destinationPathInfo.Exists)
253+
if (System.IO.Path.Exists(DestinationPath))
237254
{
238255
// Check if DestinationPath is an existing directory
239-
if (_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory))
256+
if (Directory.Exists(DestinationPath))
240257
{
241258
// Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified
242259
if (WriteMode == WriteMode.Create)
@@ -249,12 +266,12 @@ private void ValidateDestinationPath()
249266
errorCode = ErrorCode.ArchiveExistsAsDirectory;
250267
}
251268
// Throw an error if the DestinationPath is the current working directory and the cmdlet is in Overwrite mode
252-
else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo.FullName == SessionState.Path.CurrentFileSystemLocation.ProviderPath)
269+
else if (WriteMode == WriteMode.Overwrite && DestinationPath == SessionState.Path.CurrentFileSystemLocation.ProviderPath)
253270
{
254271
errorCode = ErrorCode.CannotOverwriteWorkingDirectory;
255272
}
256273
// Throw an error if the DestinationPath is a directory with at 1 least item and the cmdlet is in Overwrite mode
257-
else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo is DirectoryInfo directory && directory.GetFileSystemInfos().Length > 0)
274+
else if (WriteMode == WriteMode.Overwrite && Directory.GetFileSystemEntries(DestinationPath).Length > 0)
258275
{
259276
errorCode = ErrorCode.ArchiveIsNonEmptyDirectory;
260277
}
@@ -268,7 +285,7 @@ private void ValidateDestinationPath()
268285
errorCode = ErrorCode.ArchiveExists;
269286
}
270287
// Throw an error if the cmdlet is in Update mode but the archive is read only
271-
else if (WriteMode == WriteMode.Update && _destinationPathInfo.Attributes.HasFlag(FileAttributes.ReadOnly))
288+
else if (WriteMode == WriteMode.Update && File.GetAttributes(DestinationPath).HasFlag(FileAttributes.ReadOnly))
272289
{
273290
errorCode = ErrorCode.ArchiveReadOnly;
274291
}
@@ -283,7 +300,7 @@ private void ValidateDestinationPath()
283300
if (errorCode is not null)
284301
{
285302
// Throw an error -- since we are validating DestinationPath, the problem is with DestinationPath
286-
var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: _destinationPathInfo.FullName);
303+
var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: DestinationPath);
287304
ThrowTerminatingError(errorRecord);
288305
}
289306

@@ -293,37 +310,39 @@ private void ValidateDestinationPath()
293310

294311
private void DeleteDestinationPathIfExists()
295312
{
296-
Debug.Assert(_destinationPathInfo is not null);
297313
try
298314
{
299315
// No need to ensure DestinationPath has no children when deleting it
300316
// because ValidateDestinationPath should have already done this
301-
if (_destinationPathInfo.Exists)
317+
if (File.Exists(DestinationPath))
302318
{
303-
_destinationPathInfo.Delete();
319+
File.Delete(DestinationPath);
320+
}
321+
else if (Directory.Exists(DestinationPath))
322+
{
323+
Directory.Delete(DestinationPath);
304324
}
305325
}
306326
// Throw a terminating error if an IOException occurs
307327
catch (IOException ioException)
308328
{
309329
var errorRecord = new ErrorRecord(ioException, errorId: nameof(ErrorCode.OverwriteDestinationPathFailed),
310-
errorCategory: ErrorCategory.InvalidOperation, targetObject: _destinationPathInfo.FullName);
330+
errorCategory: ErrorCategory.InvalidOperation, targetObject: DestinationPath);
311331
ThrowTerminatingError(errorRecord);
312332
}
313333
// Throw a terminating error if an UnauthorizedAccessException occurs
314334
catch (System.UnauthorizedAccessException unauthorizedAccessException)
315335
{
316336
var errorRecord = new ErrorRecord(unauthorizedAccessException, errorId: nameof(ErrorCode.InsufficientPermissionsToAccessPath),
317-
errorCategory: ErrorCategory.PermissionDenied, targetObject: _destinationPathInfo.FullName);
337+
errorCategory: ErrorCategory.PermissionDenied, targetObject: DestinationPath);
318338
ThrowTerminatingError(errorRecord);
319339
}
320340
}
321341

322342
private void DetermineArchiveFormat()
323343
{
324-
Debug.Assert(_destinationPathInfo is not null);
325344
// Check if cmdlet is able to determine the format of the archive based on the extension of DestinationPath
326-
bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatFromExtension(path: _destinationPathInfo.FullName, archiveFormat: out var archiveFormat);
345+
bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatFromExtension(path: DestinationPath, archiveFormat: out var archiveFormat);
327346
// If the user did not specify which archive format to use, try to determine it automatically
328347
if (Format is null)
329348
{
@@ -334,7 +353,7 @@ private void DetermineArchiveFormat()
334353
else
335354
{
336355
// If the archive format could not be determined, use zip by default and emit a warning
337-
var warningMsg = string.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, _destinationPathInfo.FullName);
356+
var warningMsg = string.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, DestinationPath);
338357
WriteWarning(warningMsg);
339358
Format = ArchiveFormat.Zip;
340359
}
@@ -347,10 +366,21 @@ private void DetermineArchiveFormat()
347366
{
348367
if (archiveFormat is null || archiveFormat.Value != Format.Value)
349368
{
350-
var warningMsg = string.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, _destinationPathInfo.FullName);
369+
var warningMsg = string.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, DestinationPath);
351370
WriteWarning(warningMsg);
352371
}
353372
}
354373
}
374+
375+
// Adds a path to _paths variable
376+
// If the path being added is a duplicate, it adds it _duplicatePaths (if it is not already there)
377+
// If the path is the same as the destination path, it sets _isSourcePathEqualToDestinationPath to true
378+
private void AddPathToPaths(string pathToAdd) {
379+
if (!_paths.Add(pathToAdd)) {
380+
_duplicatePaths.Add(pathToAdd);
381+
} else if (!_isSourcePathEqualToDestinationPath && pathToAdd == DestinationPath) {
382+
_isSourcePathEqualToDestinationPath = true;
383+
}
384+
}
355385
}
356386
}

src/ErrorMessages.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ internal static ErrorRecord GetErrorRecord(ErrorCode errorCode, string errorItem
1616
return new ErrorRecord(exception, errorCode.ToString(), ErrorCategory.InvalidArgument, errorItem);
1717
}
1818

19+
internal static ErrorRecord GetErrorRecord(ErrorCode errorCode)
20+
{
21+
var errorMsg = GetErrorMessage(errorCode: errorCode);
22+
var exception = new ArgumentException(errorMsg);
23+
return new ErrorRecord(exception, errorCode.ToString(), ErrorCategory.InvalidArgument, null);
24+
}
25+
1926
internal static string GetErrorMessage(ErrorCode errorCode)
2027
{
2128
return errorCode switch

src/Localized/Messages.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@
175175
<value>{0}% complete</value>
176176
</data>
177177
<data name="SameLiteralPathAndDestinationPathMessage" xml:space="preserve">
178-
<value>A path {0} supplied to -LiteralPath is the same as the path supplied to -DestinationPath.</value>
178+
<value>A path supplied to -LiteralPath is the same as the path supplied to -DestinationPath.</value>
179179
</data>
180180
<data name="SamePathAndDestinationPathMessage" xml:space="preserve">
181-
<value>A path {0} supplied to -Path is the same as the path supplied to -DestinationPath.</value>
181+
<value>A path supplied to -Path is the same as the path supplied to -DestinationPath.</value>
182182
</data>
183183
</root>

0 commit comments

Comments
 (0)