1+ using System ;
2+ using System . IO ;
3+ using System . IO . Compression ;
4+ using System . Linq ;
5+ using System . Threading . Tasks ;
6+
7+ namespace GeneralUpdate . Tool . Avalonia . Common ;
8+
9+ /// <summary>
10+ /// Utility class for zip file compression operations
11+ /// </summary>
12+ public static class ZipUtility
13+ {
14+ /// <summary>
15+ /// Characters that are invalid in file names across all platforms
16+ /// Includes platform-specific invalid chars and common problematic characters
17+ /// </summary>
18+ private static readonly char [ ] InvalidFileNameChars =
19+ Path . GetInvalidFileNameChars ( )
20+ . Concat ( new [ ] { '<' , '>' , ':' , '"' , '/' , '\\ ' , '|' , '?' , '*' } )
21+ . Distinct ( )
22+ . ToArray ( ) ;
23+
24+ /// <summary>
25+ /// Sanitizes a string to be used as a filename by replacing invalid characters
26+ /// </summary>
27+ /// <param name="fileName">The filename to sanitize</param>
28+ /// <param name="replacement">The replacement character for invalid characters (default: '_')</param>
29+ /// <returns>Sanitized filename</returns>
30+ public static string SanitizeFileName ( string fileName , char replacement = '_' )
31+ {
32+ if ( string . IsNullOrWhiteSpace ( fileName ) )
33+ return fileName ;
34+
35+ var sanitized = fileName ;
36+ foreach ( var invalidChar in InvalidFileNameChars )
37+ {
38+ sanitized = sanitized . Replace ( invalidChar , replacement ) ;
39+ }
40+
41+ return sanitized ;
42+ }
43+ /// <summary>
44+ /// Compresses a directory into a zip file
45+ /// </summary>
46+ /// <param name="sourceDirectory">Source directory to compress</param>
47+ /// <param name="destinationZipFile">Destination zip file path</param>
48+ /// <param name="compressionLevel">Compression level (default: Optimal)</param>
49+ /// <param name="includeBaseDirectory">Whether to include the base directory in the archive</param>
50+ /// <exception cref="ArgumentNullException">Thrown when sourceDirectory or destinationZipFile is null or empty</exception>
51+ /// <exception cref="DirectoryNotFoundException">Thrown when sourceDirectory does not exist</exception>
52+ public static void CompressDirectory (
53+ string sourceDirectory ,
54+ string destinationZipFile ,
55+ CompressionLevel compressionLevel = CompressionLevel . Optimal ,
56+ bool includeBaseDirectory = false )
57+ {
58+ if ( string . IsNullOrWhiteSpace ( sourceDirectory ) )
59+ throw new ArgumentNullException ( nameof ( sourceDirectory ) ) ;
60+
61+ if ( string . IsNullOrWhiteSpace ( destinationZipFile ) )
62+ throw new ArgumentNullException ( nameof ( destinationZipFile ) ) ;
63+
64+ if ( ! Directory . Exists ( sourceDirectory ) )
65+ throw new DirectoryNotFoundException ( $ "Source directory not found: { sourceDirectory } ") ;
66+
67+ // Ensure the destination directory exists
68+ var destinationDir = Path . GetDirectoryName ( destinationZipFile ) ;
69+ if ( ! string . IsNullOrEmpty ( destinationDir ) && ! Directory . Exists ( destinationDir ) )
70+ {
71+ Directory . CreateDirectory ( destinationDir ) ;
72+ }
73+
74+ // Delete existing zip file if it exists
75+ if ( File . Exists ( destinationZipFile ) )
76+ {
77+ File . Delete ( destinationZipFile ) ;
78+ }
79+
80+ // Create the zip archive
81+ ZipFile . CreateFromDirectory ( sourceDirectory , destinationZipFile , compressionLevel , includeBaseDirectory ) ;
82+ }
83+
84+ /// <summary>
85+ /// Compresses a directory into a zip file asynchronously
86+ /// </summary>
87+ /// <param name="sourceDirectory">Source directory to compress</param>
88+ /// <param name="destinationZipFile">Destination zip file path</param>
89+ /// <param name="compressionLevel">Compression level (default: Optimal)</param>
90+ /// <param name="includeBaseDirectory">Whether to include the base directory in the archive</param>
91+ /// <returns>Task representing the asynchronous operation</returns>
92+ /// <exception cref="ArgumentNullException">Thrown when sourceDirectory or destinationZipFile is null or empty</exception>
93+ /// <exception cref="DirectoryNotFoundException">Thrown when sourceDirectory does not exist</exception>
94+ public static Task CompressDirectoryAsync (
95+ string sourceDirectory ,
96+ string destinationZipFile ,
97+ CompressionLevel compressionLevel = CompressionLevel . Optimal ,
98+ bool includeBaseDirectory = false )
99+ {
100+ return Task . Run ( ( ) => CompressDirectory ( sourceDirectory , destinationZipFile , compressionLevel , includeBaseDirectory ) ) ;
101+ }
102+
103+ /// <summary>
104+ /// Extracts a zip file to a directory
105+ /// </summary>
106+ /// <param name="sourceZipFile">Source zip file to extract</param>
107+ /// <param name="destinationDirectory">Destination directory for extraction</param>
108+ /// <param name="overwriteFiles">Whether to overwrite existing files</param>
109+ /// <exception cref="ArgumentNullException">Thrown when sourceZipFile or destinationDirectory is null or empty</exception>
110+ /// <exception cref="FileNotFoundException">Thrown when sourceZipFile does not exist</exception>
111+ /// <exception cref="InvalidOperationException">Thrown when a zip entry attempts to extract outside the destination directory (zip slip attack)</exception>
112+ public static void ExtractZipFile (
113+ string sourceZipFile ,
114+ string destinationDirectory ,
115+ bool overwriteFiles = true )
116+ {
117+ if ( string . IsNullOrWhiteSpace ( sourceZipFile ) )
118+ throw new ArgumentNullException ( nameof ( sourceZipFile ) ) ;
119+
120+ if ( string . IsNullOrWhiteSpace ( destinationDirectory ) )
121+ throw new ArgumentNullException ( nameof ( destinationDirectory ) ) ;
122+
123+ if ( ! File . Exists ( sourceZipFile ) )
124+ throw new FileNotFoundException ( $ "Source zip file not found: { sourceZipFile } ") ;
125+
126+ // Ensure the destination directory exists
127+ if ( ! Directory . Exists ( destinationDirectory ) )
128+ {
129+ Directory . CreateDirectory ( destinationDirectory ) ;
130+ }
131+
132+ // Get the normalized full path of the destination directory
133+ var normalizedDestination = Path . GetFullPath ( destinationDirectory ) ;
134+
135+ // Extract the zip archive with zip slip protection
136+ using ( var archive = System . IO . Compression . ZipFile . OpenRead ( sourceZipFile ) )
137+ {
138+ foreach ( var entry in archive . Entries )
139+ {
140+ // Get the full path where the entry will be extracted
141+ var entryPath = Path . Combine ( destinationDirectory , entry . FullName ) ;
142+ var normalizedEntryPath = Path . GetFullPath ( entryPath ) ;
143+
144+ // Validate that the entry path is within the destination directory (zip slip protection)
145+ if ( ! normalizedEntryPath . StartsWith ( normalizedDestination , StringComparison . OrdinalIgnoreCase ) )
146+ {
147+ throw new InvalidOperationException (
148+ $ "Zip entry '{ entry . FullName } ' attempts to extract outside the destination directory. " +
149+ "This may indicate a zip slip attack." ) ;
150+ }
151+
152+ // Create directory for the entry if needed
153+ if ( string . IsNullOrEmpty ( entry . Name ) )
154+ {
155+ // This is a directory entry
156+ Directory . CreateDirectory ( normalizedEntryPath ) ;
157+ }
158+ else
159+ {
160+ // This is a file entry
161+ var entryDirectory = Path . GetDirectoryName ( normalizedEntryPath ) ;
162+ if ( ! string . IsNullOrEmpty ( entryDirectory ) && ! Directory . Exists ( entryDirectory ) )
163+ {
164+ Directory . CreateDirectory ( entryDirectory ) ;
165+ }
166+
167+ // Extract the file
168+ entry . ExtractToFile ( normalizedEntryPath , overwriteFiles ) ;
169+ }
170+ }
171+ }
172+ }
173+
174+ /// <summary>
175+ /// Extracts a zip file to a directory asynchronously
176+ /// </summary>
177+ /// <param name="sourceZipFile">Source zip file to extract</param>
178+ /// <param name="destinationDirectory">Destination directory for extraction</param>
179+ /// <param name="overwriteFiles">Whether to overwrite existing files</param>
180+ /// <returns>Task representing the asynchronous operation</returns>
181+ /// <exception cref="ArgumentNullException">Thrown when sourceZipFile or destinationDirectory is null or empty</exception>
182+ /// <exception cref="FileNotFoundException">Thrown when sourceZipFile does not exist</exception>
183+ /// <exception cref="InvalidOperationException">Thrown when a zip entry attempts to extract outside the destination directory (zip slip attack)</exception>
184+ public static Task ExtractZipFileAsync (
185+ string sourceZipFile ,
186+ string destinationDirectory ,
187+ bool overwriteFiles = true )
188+ {
189+ return Task . Run ( ( ) => ExtractZipFile ( sourceZipFile , destinationDirectory , overwriteFiles ) ) ;
190+ }
191+
192+ /// <summary>
193+ /// Adds a file to an existing zip archive
194+ /// </summary>
195+ /// <param name="zipFilePath">Path to the zip file</param>
196+ /// <param name="entryName">Entry name in the archive</param>
197+ /// <param name="content">Content to add</param>
198+ /// <exception cref="ArgumentNullException">Thrown when parameters are null or empty</exception>
199+ /// <exception cref="ArgumentException">Thrown when parameters are empty or whitespace</exception>
200+ /// <exception cref="FileNotFoundException">Thrown when zipFilePath does not exist</exception>
201+ public static void AddFileToZip ( string zipFilePath , string entryName , string content )
202+ {
203+ if ( string . IsNullOrWhiteSpace ( zipFilePath ) )
204+ throw new ArgumentException ( "Zip file path cannot be null or empty" , nameof ( zipFilePath ) ) ;
205+
206+ if ( string . IsNullOrWhiteSpace ( entryName ) )
207+ throw new ArgumentException ( "Entry name cannot be null or empty" , nameof ( entryName ) ) ;
208+
209+ if ( content == null )
210+ throw new ArgumentNullException ( nameof ( content ) ) ;
211+
212+ if ( ! File . Exists ( zipFilePath ) )
213+ throw new FileNotFoundException ( $ "Zip file not found: { zipFilePath } ") ;
214+
215+ using ( var archive = System . IO . Compression . ZipFile . Open ( zipFilePath , ZipArchiveMode . Update ) )
216+ {
217+ // Remove existing entry if it exists
218+ var existingEntry = archive . GetEntry ( entryName ) ;
219+ existingEntry ? . Delete ( ) ;
220+
221+ // Create new entry
222+ var entry = archive . CreateEntry ( entryName , CompressionLevel . Optimal ) ;
223+ using ( var writer = new StreamWriter ( entry . Open ( ) ) )
224+ {
225+ writer . Write ( content ) ;
226+ }
227+ }
228+ }
229+
230+ /// <summary>
231+ /// Adds a file to an existing zip archive asynchronously
232+ /// </summary>
233+ /// <param name="zipFilePath">Path to the zip file</param>
234+ /// <param name="entryName">Entry name in the archive</param>
235+ /// <param name="content">Content to add</param>
236+ /// <returns>Task representing the asynchronous operation</returns>
237+ /// <exception cref="ArgumentNullException">Thrown when parameters are null</exception>
238+ /// <exception cref="ArgumentException">Thrown when parameters are empty or whitespace</exception>
239+ /// <exception cref="FileNotFoundException">Thrown when zipFilePath does not exist</exception>
240+ public static Task AddFileToZipAsync ( string zipFilePath , string entryName , string content )
241+ {
242+ return Task . Run ( ( ) => AddFileToZip ( zipFilePath , entryName , content ) ) ;
243+ }
244+ }
0 commit comments