77using System . Diagnostics . CodeAnalysis ;
88using System . IO ;
99using System . IO . Compression ;
10- using System . Linq ;
1110using System . Management . Automation ;
11+ using System . Runtime . InteropServices ;
1212
1313using 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}
0 commit comments