using AIK.Common.SysCommon; using AIK.Service.IService; using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Readers; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; namespace AIK.Service.Service { public class FileOperationService : IFileOperationService { public async Task ExtractSingleArchiveAsync( string archivePath, string destinationDir, bool overwrite = true, CancellationToken cancellationToken = default) { if (!Directory.Exists(destinationDir)) Directory.CreateDirectory(destinationDir); try { using var archive = ArchiveFactory.Open(archivePath); var allEntries = archive.Entries .Where(e => !e.IsDirectory) .ToList(); bool shouldStripReceivedData = HasRootReceivedDataFolder(allEntries); foreach (var entry in allEntries) { cancellationToken.ThrowIfCancellationRequested(); string normalizedEntryPath = NormalizeArchivePath(entry.Key); string targetRelativePath; if (shouldStripReceivedData) { if (normalizedEntryPath.StartsWith($"{ComStringHelper.K3ToolDirectory}/", StringComparison.OrdinalIgnoreCase)) { targetRelativePath = normalizedEntryPath.Substring($"{ComStringHelper.K3ToolDirectory}/".Length); } else { continue; // 跳过非 received_data 内容 } } else { targetRelativePath = normalizedEntryPath; } // 安全校验(GetSafeExtractionPath 内部也应基于规范化路径) string destPath = GetSafeExtractionPath(destinationDir, targetRelativePath); Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); await Task.Run(() => { entry.WriteToFile(destPath, new ExtractionOptions { Overwrite = overwrite, PreserveFileTime = false }); }, cancellationToken); } } catch (Exception ex) { throw new InvalidOperationException($"解压失败: {archivePath}", ex); } } public async Task ExtractMultipleArchivesAsyncNotPoress( IEnumerable archivePaths, string destinationDir, bool overwrite = true, CancellationToken cancellationToken = default) { if (!Directory.Exists(destinationDir)) Directory.CreateDirectory(destinationDir); foreach (string path in archivePaths) { await ExtractSingleArchiveAsync(path, destinationDir, overwrite, cancellationToken); } } public async Task ExtractMultipleArchivesAsync( IReadOnlyList archivePaths, string destDir, IProgress progress, bool overwrite, CancellationToken cancellationToken) { int total = archivePaths.Count; if (total == 0) { progress.Report(100); return; } for (int i = 0; i < total; i++) { cancellationToken.ThrowIfCancellationRequested(); string path = archivePaths[i]; if (File.Exists(path)) { await Task.Run(() => { System.IO.Compression.ZipFile.ExtractToDirectory(path, destDir); }, cancellationToken); } double percent = (i + 1.0) / total * 100.0; progress.Report(percent); } progress.Report(100); } // 私有工具方法 private static string GetSafeExtractionPath(string baseDir, string normalizedRelativePath) { // 此时 normalizedRelativePath 已是 clean 格式,如 "bin/app.exe" if (string.IsNullOrEmpty(normalizedRelativePath)) throw new ArgumentException("路径为空"); // 拼接并获取完整路径 string fullPath = Path.GetFullPath(Path.Combine(baseDir, normalizedRelativePath)); // 安全校验:必须在 baseDir 内 string baseDirNormalized = Path.GetFullPath(baseDir) + Path.DirectorySeparatorChar; if (!fullPath.StartsWith(baseDirNormalized, StringComparison.OrdinalIgnoreCase)) { throw new SecurityException($"非法路径遍历: {normalizedRelativePath}"); } // 仅对文件名部分处理非法字符 string dirPart = Path.GetDirectoryName(fullPath)!; string filePart = Path.GetFileName(fullPath); string safeFilePart = Path.GetInvalidFileNameChars() .Aggregate(filePart, (current, c) => current.Replace(c.ToString(), "_")); return Path.Combine(dirPart, safeFilePart); } private static bool HasRootReceivedDataFolder(List entries) { return entries.Any(e => e.Key.Replace('\\', '/').StartsWith($"{ComStringHelper.K3ToolDirectory}/", StringComparison.OrdinalIgnoreCase)); } private static string NormalizeArchivePath(string archivePath) { if (string.IsNullOrEmpty(archivePath)) return string.Empty; // 1. 统一斜杠 string path = archivePath.Replace('\\', '/'); // 2. 分割并过滤危险/冗余段 var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) .Where(part => part != "." && !part.Equals("..", StringComparison.OrdinalIgnoreCase)) .ToList(); // 3. 重建路径(无开头/结尾斜杠) return parts.Count == 0 ? string.Empty : string.Join("/", parts); } } }