Skip to content

Commit 714a5e0

Browse files
authored
add extension func (#5)
1 parent d2ef908 commit 714a5e0

9 files changed

Lines changed: 1364 additions & 2 deletions

src/Common/ZipUtility.cs

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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+
}

src/Models/CustomPropertyModel.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using CommunityToolkit.Mvvm.ComponentModel;
2+
3+
namespace GeneralUpdate.Tool.Avalonia.Models;
4+
5+
public class CustomPropertyModel : ObservableObject
6+
{
7+
private string _key;
8+
private string _value;
9+
10+
/// <summary>
11+
/// Property key
12+
/// </summary>
13+
public string Key
14+
{
15+
get => _key;
16+
set => SetProperty(ref _key, value);
17+
}
18+
19+
/// <summary>
20+
/// Property value
21+
/// </summary>
22+
public string Value
23+
{
24+
get => _value;
25+
set => SetProperty(ref _value, value);
26+
}
27+
}

0 commit comments

Comments
 (0)