using ICSharpCode.SharpZipLib.Zip; using System.Collections.Generic; using System.IO; using UnityEngine; public class ZipWrapper : MonoBehaviour { #region ZipCallback public abstract class ZipCallback { /// /// ѹ�������ļ����ļ���ǰִ�еĻص� /// /// /// �������true����ѹ���ļ����ļ��У���֮��ѹ���ļ����ļ��� public virtual bool OnPreZip(ZipEntry _entry) { return true; } /// /// ѹ�������ļ����ļ��к�ִ�еĻص� /// /// public virtual void OnPostZip(ZipEntry _entry) { Debug.Log("OnPostZip: " + _entry.Name); } /// /// ѹ��ִ����Ϻ�Ļص� /// /// true��ʾѹ���ɹ���false��ʾѹ��ʧ�� public virtual void OnFinished(bool _result) { } } #endregion #region UnzipCallback public abstract class UnzipCallback { /// /// ��ѹ�����ļ����ļ���ǰִ�еĻص� /// /// /// �������true����ѹ���ļ����ļ��У���֮��ѹ���ļ����ļ��� public virtual bool OnPreUnzip(ZipEntry _entry) { return true; } /// /// ��ѹ�����ļ����ļ��к�ִ�еĻص� /// /// public virtual void OnPostUnzip(ZipEntry _entry) { } /// /// ��ѹִ����Ϻ�Ļص� /// /// true��ʾ��ѹ�ɹ���false��ʾ��ѹʧ�� public virtual void OnFinished(bool _result) { } } #endregion /// /// ѹ���ļ����ļ��� /// /// �ļ���·�����ļ��� /// ѹ��������·���ļ��� /// ѹ������ /// ZipCallback���󣬸���ص� /// public static bool Zip(List _fileOrDirectoryArray, string _outputPathName, string _password = null, ZipCallback _zipCallback = null) { if ((null == _fileOrDirectoryArray) || string.IsNullOrEmpty(_outputPathName)) { if (null != _zipCallback) _zipCallback.OnFinished(false); return false; } // 使用 using 语句确保 FileStream 和 ZipOutputStream 资源被正确释放,即使在异常情况下也能释放 // 注意:Unity 平台不支持 System.Security.AccessControl,因此使用标准文件创建方式 // 在实际部署时,建议通过操作系统级别的文件权限设置来限制文件访问 using (FileStream fileStream = File.Create(_outputPathName)) { using (ZipOutputStream zipOutputStream = new ZipOutputStream(fileStream)) { zipOutputStream.SetLevel(6); // 压缩级别,压缩率与压缩速度的平衡点 if (!string.IsNullOrEmpty(_password)) zipOutputStream.Password = _password; for (int index = 0; index < _fileOrDirectoryArray.Count; ++index) { bool result = false; string fileOrDirectory = _fileOrDirectoryArray[index]; if (Directory.Exists(fileOrDirectory)) result = ZipDirectory(fileOrDirectory, string.Empty, zipOutputStream, _zipCallback); else if (File.Exists(fileOrDirectory)) result = ZipFile(fileOrDirectory, string.Empty, zipOutputStream, _zipCallback); if (!result) { if (null != _zipCallback) _zipCallback.OnFinished(false); return false; } } zipOutputStream.Finish(); zipOutputStream.Close(); if (null != _zipCallback) _zipCallback.OnFinished(true); return true; } } } /// /// ��ѹZip�� /// /// Zip�����ļ�·���� /// ��ѹ���·�� /// ��ѹ���� /// UnzipCallback���󣬸���ص� /// public static bool UnzipFile(string _filePathName, string _outputPath, string _password = null, UnzipCallback _unzipCallback = null) { if (string.IsNullOrEmpty(_filePathName) || string.IsNullOrEmpty(_outputPath)) { if (null != _unzipCallback) _unzipCallback.OnFinished(false); return false; } try { // 使用 using 语句确保 FileStream 资源被正确释放 using (FileStream fileStream = File.OpenRead(_filePathName)) { return UnzipFile(fileStream, _outputPath, _password, _unzipCallback); } } catch (System.Exception _e) { Debug.LogError("[ZipUtility.UnzipFile]: " + _e.ToString()); if (null != _unzipCallback) _unzipCallback.OnFinished(false); return false; } } /// /// ��ѹZip�� /// /// Zip���ֽ����� /// ��ѹ���·�� /// ��ѹ���� /// UnzipCallback���󣬸���ص� /// public static bool UnzipFile(byte[] _fileBytes, string _outputPath, string _password = null, UnzipCallback _unzipCallback = null) { if ((null == _fileBytes) || string.IsNullOrEmpty(_outputPath)) { if (null != _unzipCallback) _unzipCallback.OnFinished(false); return false; } bool result = UnzipFile(new MemoryStream(_fileBytes), _outputPath, _password, _unzipCallback); if (!result) { if (null != _unzipCallback) _unzipCallback.OnFinished(false); } return result; } /// /// 清理和验证 ZIP 条目名称,防止路径遍历攻击 /// /// ZIP 条目名称(可能包含路径遍历字符) /// 输出目录路径 /// 清理后的安全路径,如果路径不安全则返回 null private static string SanitizeZipEntryName(string entryName, string outputPath) { if (string.IsNullOrEmpty(entryName)) { return null; } // 规范化路径分隔符(统一使用正斜杠或反斜杠) string normalizedName = entryName.Replace('\\', '/'); // 移除路径遍历字符(.. 和 .) string[] parts = normalizedName.Split('/'); List safeParts = new List(); foreach (string part in parts) { if (string.IsNullOrEmpty(part) || part == ".") { // 跳过空字符串和当前目录引用 continue; } else if (part == "..") { // 如果遇到父目录引用,移除最后一个安全部分(如果存在) if (safeParts.Count > 0) { safeParts.RemoveAt(safeParts.Count - 1); } else { // 如果已经在根目录,则忽略路径遍历尝试 // 这表示恶意路径遍历,返回 null 表示拒绝 return null; } } else { // 验证路径片段是否安全:检查是否包含路径遍历字符或绝对路径标识符 // 防止恶意构造的路径片段(如 "C:" 或包含 ".." 的片段) if (part.Contains("..") || part.Contains("\\") || part.Contains(":")) { Debug.LogWarning($"[ZipWrapper.SanitizeZipEntryName] 检测到不安全的路径片段: {part}"); return null; } // 验证路径片段是否为绝对路径(Windows 驱动器号) if (part.Length >= 2 && part[1] == ':' && part[0] >= 'A' && part[0] <= 'Z') { Debug.LogWarning($"[ZipWrapper.SanitizeZipEntryName] 检测到绝对路径片段(驱动器号): {part}"); return null; } // 添加到安全部分列表 safeParts.Add(part); } } if (safeParts.Count == 0) { return null; } // 重新组合安全路径 string safePath = string.Join(Path.DirectorySeparatorChar.ToString(), safeParts.ToArray()); // 额外验证:确保 safePath 不包含路径遍历字符(防御性检查) // 检查是否包含路径遍历字符或绝对路径标识符 if (safePath.Contains("..") || (safePath.Length >= 2 && safePath[1] == ':' && safePath[0] >= 'A' && safePath[0] <= 'Z')) { Debug.LogWarning($"[ZipWrapper.SanitizeZipEntryName] 检测到不安全的路径片段: {safePath}"); return null; } // 在 Path.Combine 之前,额外验证 safePath 是否为绝对路径 // 如果 safePath 是绝对路径,则拒绝,防止路径遍历攻击 if (Path.IsPathRooted(safePath)) { Debug.LogWarning($"[ZipWrapper.SanitizeZipEntryName] 检测到绝对路径,拒绝路径遍历攻击: {entryName} -> {safePath}"); return null; } // 与输出路径组合(使用已清理的安全路径) string fullPath = Path.Combine(outputPath, safePath); // 规范化最终路径 fullPath = Path.GetFullPath(fullPath); string normalizedOutputPath = Path.GetFullPath(outputPath); // 验证最终路径是否在输出目录内(防止路径遍历攻击) if (!fullPath.StartsWith(normalizedOutputPath + Path.DirectorySeparatorChar, System.StringComparison.Ordinal) && fullPath != normalizedOutputPath) { // 路径不在预期目录内,拒绝访问 Debug.LogWarning($"[ZipWrapper.SanitizeZipEntryName] 检测到路径遍历攻击尝试: {entryName} -> {fullPath}"); return null; } return fullPath; } /// /// ��ѹZip�� /// /// Zip�������� /// ��ѹ���·�� /// ��ѹ���� /// UnzipCallback���󣬸���ص� /// public static bool UnzipFile(Stream _inputStream, string _outputPath, string _password = null, UnzipCallback _unzipCallback = null) { if ((null == _inputStream) || string.IsNullOrEmpty(_outputPath)) { if (null != _unzipCallback) _unzipCallback.OnFinished(false); return false; } // �����ļ�Ŀ¼ if (!Directory.Exists(_outputPath)) Directory.CreateDirectory(_outputPath); // ��ѹZip�� ZipEntry entry = null; using (ZipInputStream zipInputStream = new ZipInputStream(_inputStream)) { if (!string.IsNullOrEmpty(_password)) zipInputStream.Password = _password; while (null != (entry = zipInputStream.GetNextEntry())) { if (string.IsNullOrEmpty(entry.Name)) continue; if ((null != _unzipCallback) && !_unzipCallback.OnPreUnzip(entry)) continue; // ���� // 使用安全验证方法清理和验证 ZIP 条目名称,防止路径遍历攻击 string filePathName = SanitizeZipEntryName(entry.Name, _outputPath); if (filePathName == null) { // 路径不安全,跳过此条目 Debug.LogWarning($"[ZipWrapper.UnzipFile] 跳过不安全的 ZIP 条目: {entry.Name}"); continue; } // 额外的安全验证:在使用 filePathName 创建文件前,再次验证路径的安全性 // 确保 filePathName 是规范化后的绝对路径,且确实在输出目录内 string normalizedFilePath = Path.GetFullPath(filePathName); string normalizedOutputPath = Path.GetFullPath(_outputPath); // 验证路径是否在输出目录内(防止路径遍历攻击) if (!normalizedFilePath.StartsWith(normalizedOutputPath + Path.DirectorySeparatorChar, System.StringComparison.OrdinalIgnoreCase) && normalizedFilePath != normalizedOutputPath) { Debug.LogWarning($"[ZipWrapper.UnzipFile] 检测到路径遍历攻击,拒绝访问: {entry.Name} -> {normalizedFilePath}"); continue; } // 确保路径中不包含不安全的字符 if (normalizedFilePath.Contains("..") || Path.IsPathRooted(entry.Name)) { Debug.LogWarning($"[ZipWrapper.UnzipFile] 检测到不安全的路径字符,拒绝访问: {entry.Name}"); continue; } // 创建父目录(如果需要) string directoryPath = Path.GetDirectoryName(normalizedFilePath); if (!string.IsNullOrEmpty(directoryPath)) { // 再次验证父目录路径的安全性,确保在输出目录内 string normalizedDirectoryPath = Path.GetFullPath(directoryPath); if (normalizedDirectoryPath.StartsWith(normalizedOutputPath + Path.DirectorySeparatorChar, System.StringComparison.OrdinalIgnoreCase) || normalizedDirectoryPath == normalizedOutputPath) { if (!Directory.Exists(normalizedDirectoryPath)) { Directory.CreateDirectory(normalizedDirectoryPath); } } else { Debug.LogWarning($"[ZipWrapper.UnzipFile] 父目录路径不在输出目录内,拒绝创建: {directoryPath}"); continue; } } // 如果是目录条目,创建目录后跳过 if (entry.IsDirectory) { // 再次验证目录路径的安全性,确保在输出目录内 if (normalizedFilePath.StartsWith(normalizedOutputPath + Path.DirectorySeparatorChar, System.StringComparison.OrdinalIgnoreCase) || normalizedFilePath == normalizedOutputPath) { Directory.CreateDirectory(normalizedFilePath); } else { Debug.LogWarning($"[ZipWrapper.UnzipFile] 目录路径不在输出目录内,拒绝创建: {normalizedFilePath}"); } continue; } // 写入文件 try { // 最终安全验证:在使用 File.Create 前,最后一次验证路径的安全性 // 确保 normalizedFilePath 仍然是规范化的绝对路径,且在输出目录内 string finalFilePath = Path.GetFullPath(normalizedFilePath); string finalOutputPath = Path.GetFullPath(_outputPath); // 再次验证路径是否在输出目录内(防止路径遍历攻击) if (!finalFilePath.StartsWith(finalOutputPath + Path.DirectorySeparatorChar, System.StringComparison.OrdinalIgnoreCase) && finalFilePath != finalOutputPath) { Debug.LogWarning($"[ZipWrapper.UnzipFile] 最终验证失败:路径不在输出目录内,拒绝创建文件: {entry.Name} -> {finalFilePath}"); continue; } // 确保路径中不包含路径遍历字符 if (finalFilePath.Contains("..")) { Debug.LogWarning($"[ZipWrapper.UnzipFile] 最终验证失败:检测到路径遍历字符,拒绝创建文件: {entry.Name}"); continue; } // 使用规范化后的安全路径创建文件 // 注意:Unity 平台不支持 System.Security.AccessControl,因此使用标准文件创建方式 // 在实际部署时,建议通过操作系统级别的文件权限设置来限制文件访问 using (FileStream fileStream = File.Create(finalFilePath)) { byte[] bytes = new byte[1024]; while (true) { int count = zipInputStream.Read(bytes, 0, bytes.Length); if (count > 0) fileStream.Write(bytes, 0, count); else { if (null != _unzipCallback) _unzipCallback.OnPostUnzip(entry); break; } } } } catch (System.Exception _e) { Debug.LogError("[ZipUtility.UnzipFile]: " + _e.ToString()); if (null != _unzipCallback) _unzipCallback.OnFinished(false); return false; } } } if (null != _unzipCallback) _unzipCallback.OnFinished(true); return true; } /// /// ѹ���ļ� /// /// �ļ�·���� /// Ҫѹ�����ļ��ĸ�����ļ��� /// ѹ������� /// ZipCallback���󣬸���ص� /// private static bool ZipFile(string _filePathName, string _parentRelPath, ZipOutputStream _zipOutputStream, ZipCallback _zipCallback = null) { ZipEntry entry = null; try { string entryName = _parentRelPath + '/' + Path.GetFileName(_filePathName); entry = new ZipEntry(entryName); entry.DateTime = System.DateTime.Now; // 使用 using 语句确保 FileStream 资源被正确释放 using (FileStream fileStream = File.OpenRead(_filePathName)) { byte[] buffer = new byte[fileStream.Length]; fileStream.Read(buffer, 0, buffer.Length); entry.Size = buffer.Length; _zipOutputStream.PutNextEntry(entry); _zipOutputStream.Write(buffer, 0, buffer.Length); } } catch (System.Exception _e) { Debug.LogError("[ZipUtility.ZipFile]: " + _e.ToString()); return false; } if (null != _zipCallback) _zipCallback.OnPostZip(entry); return true; } /// /// ѹ���ļ��� /// /// Ҫѹ�����ļ��� /// Ҫѹ�����ļ��еĸ�����ļ��� /// ѹ������� /// ZipCallback���󣬸���ص� /// private static bool ZipDirectory(string _path, string _parentRelPath, ZipOutputStream _zipOutputStream, ZipCallback _zipCallback = null) { ZipEntry entry = null; try { string entryName = Path.Combine(_parentRelPath, Path.GetFileName(_path) + '/'); entry = new ZipEntry(entryName); entry.DateTime = System.DateTime.Now; entry.Size = 0; _zipOutputStream.PutNextEntry(entry); _zipOutputStream.Flush(); string[] files = Directory.GetFiles(_path); for (int index = 0; index < files.Length; ++index) { // �ų�Unity�п��ܵ� .meta �ļ� if (files[index].EndsWith(".meta") == true) { Debug.LogWarning(files[index] + " not to zip"); continue; } ZipFile(files[index], Path.Combine(_parentRelPath, Path.GetFileName(_path)), _zipOutputStream, _zipCallback); } } catch (System.Exception _e) { Debug.LogError("[ZipUtility.ZipDirectory]: " + _e.ToString()); return false; } string[] directories = Directory.GetDirectories(_path); for (int index = 0; index < directories.Length; ++index) { if (!ZipDirectory(directories[index], Path.Combine(_parentRelPath, Path.GetFileName(_path)), _zipOutputStream, _zipCallback)) { return false; } } if (null != _zipCallback) _zipCallback.OnPostZip(entry); return true; } }