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) { Debug.Log("OnFinished: " + _result); }
}
#endregion
#region UnzipCallback
public abstract class UnzipCallback
{
///
/// ��ѹ�����ļ����ļ���ǰִ�еĻص�
///
///
/// �������true����ѹ���ļ����ļ��У���֮��ѹ���ļ����ļ���
public virtual bool OnPreUnzip(ZipEntry _entry)
{
return true;
}
///
/// ��ѹ�����ļ����ļ��к�ִ�еĻص�
///
///
public virtual void OnPostUnzip(ZipEntry _entry) { Debug.Log("OnPostUnzip: " + _entry.Name); }
///
/// ��ѹִ����Ϻ�Ļص�
///
/// true��ʾ��ѹ�ɹ���false��ʾ��ѹʧ��
public virtual void OnFinished(bool _result) { Debug.Log("OnFinished: " + _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;
}
}