Use FileSystem for zip extraction

Resolves #1943
This commit is contained in:
ds5678 2025-10-25 15:33:06 -07:00
parent 07d415a794
commit 2f43a7abe5
7 changed files with 204 additions and 90 deletions

View File

@ -0,0 +1,17 @@
using System.Reflection;
using System.Runtime.CompilerServices;
namespace AssetRipper.IO.Files.SourceGenerator;
internal static class ParameterExtensions
{
public static bool IsParams(this ParameterInfo parameter)
{
return parameter.GetCustomAttribute<ParamCollectionAttribute>() is not null;
}
public static string GetParamsPrefix(this ParameterInfo parameter)
{
return parameter.IsParams() ? "params " : "";
}
}

View File

@ -1,5 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
namespace AssetRipper.IO.Files.SourceGenerator;
@ -17,6 +16,7 @@ internal static class Program
WriteVirtualFileSystemClass();
}
/// <inheritdoc cref="File.WriteAllBytesAsync(string, byte[], CancellationToken)"/>
private static void WriteFileSystemClass()
{
using IndentedTextWriter writer = IndentedTextWriterFactory.Create(OutputDirectory, "FileSystem");
@ -49,9 +49,11 @@ internal static class Program
foreach (FileSystemApi api in classApiList)
{
// Inherit documentation from System.IO
writer.WriteLine($"/// <inheritdoc cref=\"{api.FullName}({api.ParametersWithoutNames})\"/>");
string virtualKeyword = api.Type is FileSystemApiType.Sealed ? "" : "virtual ";
string parametersWithTypes = string.Join(", ", api.Parameters.Select(parameter => $"{parameter.Item1} {parameter.Item2}"));
writer.WriteLine($"public {virtualKeyword}{api.BaseReturnType} {api.Name}({parametersWithTypes})");
writer.WriteLine($"public {virtualKeyword}{api.BaseReturnType} {api.Name}({api.ParametersWithTypes})");
using (new CurlyBrackets(writer))
{
if (api.Type is FileSystemApiType.Throw)
@ -61,8 +63,7 @@ internal static class Program
else
{
string returnKeyword = api.VoidReturn ? "" : "return ";
string parametersWithoutTypes = string.Join(", ", api.Parameters.Select(parameter => parameter.Item2));
writer.WriteLine($"{returnKeyword}{api.FullName}({parametersWithoutTypes});");
writer.WriteLine($"{returnKeyword}{api.FullName}({api.ParametersWithoutTypes});");
}
}
writer.WriteLineNoTabs();
@ -101,13 +102,11 @@ internal static class Program
continue;
}
string parametersWithTypes = string.Join(", ", api.Parameters.Select(parameter => $"{parameter.Item1} {parameter.Item2}"));
writer.WriteLine($"public override {api.DerivedReturnType} {api.Name}({parametersWithTypes})");
writer.WriteLine($"public override {api.DerivedReturnType} {api.Name}({api.ParametersWithTypes})");
using (new CurlyBrackets(writer))
{
string returnKeyword = api.VoidReturn ? "" : "return ";
string parametersWithoutTypes = string.Join(", ", api.Parameters.Select(parameter => parameter.Item2));
writer.WriteLine($"{returnKeyword}{api.FullName}({parametersWithoutTypes});");
writer.WriteLine($"{returnKeyword}{api.FullName}({api.ParametersWithoutTypes});");
}
writer.WriteLineNoTabs();
}
@ -170,10 +169,10 @@ internal static class Program
}
}
private static Dictionary<string, List<FileSystemApi>> apiDictionary = new()
private static readonly Dictionary<string, List<FileSystemApi>> apiDictionary = new()
{
[nameof(File)] = new()
{
[nameof(File)] =
[
new((Func<string, FileStream>)File.Create),
new(File.Delete),
new(File.Exists),
@ -185,9 +184,9 @@ internal static class Program
new((Action<string, ReadOnlySpan<byte>>)File.WriteAllBytes),
new((Action<string, ReadOnlySpan<char>>)File.WriteAllText),
new((Action<string, ReadOnlySpan<char>, Encoding>)File.WriteAllText),
},
[nameof(Directory)] = new()
{
],
[nameof(Directory)] =
[
new((Func<string, string, SearchOption, IEnumerable<string>>)Directory.EnumerateDirectories),
new((Func<string, string, SearchOption, IEnumerable<string>>)Directory.EnumerateFiles),
new((Func<string, string, SearchOption, IEnumerable<string>>)Directory.GetDirectories),
@ -201,9 +200,9 @@ internal static class Program
new((Func<string, IEnumerable<string>>)Directory.GetDirectories),
new((Func<string, IEnumerable<string>>)Directory.GetFiles),
new(Directory.Exists),
},
[nameof(Path)] = new()
{
],
[nameof(Path)] =
[
new((Func<string, string, string>)Path.Join) { Type = FileSystemApiType.Virtual },
new((Func<string, string, string, string>)Path.Join) { Type = FileSystemApiType.Virtual },
new((Func<string, string, string, string, string>)Path.Join) { Type = FileSystemApiType.Virtual },
@ -219,7 +218,7 @@ internal static class Program
new((Func<string, string>)Path.GetFullPath),
new(Path.GetRelativePath) { Type = FileSystemApiType.Sealed },
new((Func<ReadOnlySpan<char>, bool>)Path.IsPathRooted),
},
],
};
private enum FileSystemApiType
@ -228,6 +227,7 @@ internal static class Program
Virtual,
Sealed,
}
private sealed record class FileSystemApi
{
public required Delegate Delegate { get; init; }
@ -250,6 +250,7 @@ internal static class Program
.Select(parameter => (parameter.GetParamsPrefix() + parameter.ParameterType.GetGlobalQualifiedName(), parameter.Name!));
public string ParametersWithTypes => string.Join(", ", Parameters.Select(parameter => $"{parameter.Item1} {parameter.Item2}"));
public string ParametersWithoutTypes => string.Join(", ", Parameters.Select(parameter => parameter.Item2));
public string ParametersWithoutNames => string.Join(", ", Parameters.Select(parameter => parameter.Item1));
public FileSystemApi()
{
@ -262,42 +263,3 @@ internal static class Program
}
}
}
internal static class ParameterExtensions
{
public static bool IsParams(this ParameterInfo parameter)
{
return parameter.GetCustomAttribute<ParamCollectionAttribute>() is not null;
}
public static string GetParamsPrefix(this ParameterInfo parameter)
{
return parameter.IsParams() ? "params " : "";
}
}
internal static class TypeExtensions
{
public static string GetGlobalQualifiedName(this Type type)
{
if (type == typeof(void))
{
return "void";
}
else if (type.IsGenericType)
{
// Handle generic types by appending generic arguments
string genericTypeDefinition = type.GetGenericTypeDefinition().FullName!;
string genericArguments = string.Join(", ", type.GetGenericArguments()
.Select(t => t.GetGlobalQualifiedName()));
return $"global::{genericTypeDefinition[..genericTypeDefinition.IndexOf('`')]}<{genericArguments}>";
}
else if (type.IsArray)
{
// Handle arrays
return $"{type.GetElementType()!.GetGlobalQualifiedName()}[{new string(',', type.GetArrayRank() - 1)}]";
}
else
{
return $"global::{type.FullName}";
}
}
}

View File

@ -0,0 +1,28 @@
namespace AssetRipper.IO.Files.SourceGenerator;
internal static class TypeExtensions
{
public static string GetGlobalQualifiedName(this Type type)
{
if (type == typeof(void))
{
return "void";
}
else if (type.IsGenericType)
{
// Handle generic types by appending generic arguments
string genericTypeDefinition = type.GetGenericTypeDefinition().FullName!;
string genericArguments = string.Join(", ", type.GetGenericArguments().Select(GetGlobalQualifiedName));
return $"global::{genericTypeDefinition[..genericTypeDefinition.IndexOf('`')]}<{genericArguments}>";
}
else if (type.IsArray)
{
// Handle arrays
return $"{type.GetElementType()!.GetGlobalQualifiedName()}[{new string(',', type.GetArrayRank() - 1)}]";
}
else
{
return $"global::{type.FullName}";
}
}
}

View File

@ -12,56 +12,67 @@ public abstract partial class FileSystem
protected FileImplementation File => this;
protected DirectoryImplementation Directory => Parent.Directory;
protected PathImplementation Path => Parent.Path;
/// <inheritdoc cref="global::System.IO.File.Create(global::System.String)"/>
public virtual global::System.IO.Stream Create(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.Delete(global::System.String)"/>
public virtual void Delete(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.Exists(global::System.String)"/>
public virtual global::System.Boolean Exists(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.OpenRead(global::System.String)"/>
public virtual global::System.IO.Stream OpenRead(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.OpenWrite(global::System.String)"/>
public virtual global::System.IO.Stream OpenWrite(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.ReadAllBytes(global::System.String)"/>
public virtual global::System.Byte[] ReadAllBytes(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.ReadAllText(global::System.String)"/>
public virtual global::System.String ReadAllText(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.ReadAllText(global::System.String, global::System.Text.Encoding)"/>
public virtual global::System.String ReadAllText(global::System.String path, global::System.Text.Encoding encoding)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.WriteAllBytes(global::System.String, global::System.ReadOnlySpan<global::System.Byte>)"/>
public virtual void WriteAllBytes(global::System.String path, global::System.ReadOnlySpan<global::System.Byte> bytes)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.WriteAllText(global::System.String, global::System.ReadOnlySpan<global::System.Char>)"/>
public virtual void WriteAllText(global::System.String path, global::System.ReadOnlySpan<global::System.Char> contents)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.File.WriteAllText(global::System.String, global::System.ReadOnlySpan<global::System.Char>, global::System.Text.Encoding)"/>
public virtual void WriteAllText(global::System.String path, global::System.ReadOnlySpan<global::System.Char> contents, global::System.Text.Encoding encoding)
{
throw new global::System.NotSupportedException();
@ -81,66 +92,79 @@ public abstract partial class FileSystem
protected FileImplementation File => Parent.File;
protected DirectoryImplementation Directory => this;
protected PathImplementation Path => Parent.Path;
/// <inheritdoc cref="global::System.IO.Directory.EnumerateDirectories(global::System.String, global::System.String, global::System.IO.SearchOption)"/>
public virtual global::System.Collections.Generic.IEnumerable<global::System.String> EnumerateDirectories(global::System.String path, global::System.String searchPattern, global::System.IO.SearchOption searchOption)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.EnumerateFiles(global::System.String, global::System.String, global::System.IO.SearchOption)"/>
public virtual global::System.Collections.Generic.IEnumerable<global::System.String> EnumerateFiles(global::System.String path, global::System.String searchPattern, global::System.IO.SearchOption searchOption)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.GetDirectories(global::System.String, global::System.String, global::System.IO.SearchOption)"/>
public virtual global::System.String[] GetDirectories(global::System.String path, global::System.String searchPattern, global::System.IO.SearchOption searchOption)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.GetFiles(global::System.String, global::System.String, global::System.IO.SearchOption)"/>
public virtual global::System.String[] GetFiles(global::System.String path, global::System.String searchPattern, global::System.IO.SearchOption searchOption)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.EnumerateDirectories(global::System.String, global::System.String)"/>
public virtual global::System.Collections.Generic.IEnumerable<global::System.String> EnumerateDirectories(global::System.String path, global::System.String searchPattern)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.EnumerateFiles(global::System.String, global::System.String)"/>
public virtual global::System.Collections.Generic.IEnumerable<global::System.String> EnumerateFiles(global::System.String path, global::System.String searchPattern)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.GetDirectories(global::System.String, global::System.String)"/>
public virtual global::System.String[] GetDirectories(global::System.String path, global::System.String searchPattern)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.GetFiles(global::System.String, global::System.String)"/>
public virtual global::System.String[] GetFiles(global::System.String path, global::System.String searchPattern)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.EnumerateDirectories(global::System.String)"/>
public virtual global::System.Collections.Generic.IEnumerable<global::System.String> EnumerateDirectories(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.EnumerateFiles(global::System.String)"/>
public virtual global::System.Collections.Generic.IEnumerable<global::System.String> EnumerateFiles(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.GetDirectories(global::System.String)"/>
public virtual global::System.String[] GetDirectories(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.GetFiles(global::System.String)"/>
public virtual global::System.String[] GetFiles(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Directory.Exists(global::System.String)"/>
public virtual global::System.Boolean Exists(global::System.String path)
{
throw new global::System.NotSupportedException();
@ -160,76 +184,91 @@ public abstract partial class FileSystem
protected FileImplementation File => Parent.File;
protected DirectoryImplementation Directory => Parent.Directory;
protected PathImplementation Path => this;
/// <inheritdoc cref="global::System.IO.Path.Join(global::System.String, global::System.String)"/>
public virtual global::System.String Join(global::System.String path1, global::System.String path2)
{
return global::System.IO.Path.Join(path1, path2);
}
/// <inheritdoc cref="global::System.IO.Path.Join(global::System.String, global::System.String, global::System.String)"/>
public virtual global::System.String Join(global::System.String path1, global::System.String path2, global::System.String path3)
{
return global::System.IO.Path.Join(path1, path2, path3);
}
/// <inheritdoc cref="global::System.IO.Path.Join(global::System.String, global::System.String, global::System.String, global::System.String)"/>
public virtual global::System.String Join(global::System.String path1, global::System.String path2, global::System.String path3, global::System.String path4)
{
return global::System.IO.Path.Join(path1, path2, path3, path4);
}
/// <inheritdoc cref="global::System.IO.Path.Join(params global::System.ReadOnlySpan<global::System.String>)"/>
public virtual global::System.String Join(params global::System.ReadOnlySpan<global::System.String> paths)
{
return global::System.IO.Path.Join(paths);
}
/// <inheritdoc cref="global::System.IO.Path.GetDirectoryName(global::System.ReadOnlySpan<global::System.Char>)"/>
public global::System.ReadOnlySpan<global::System.Char> GetDirectoryName(global::System.ReadOnlySpan<global::System.Char> path)
{
return global::System.IO.Path.GetDirectoryName(path);
}
/// <inheritdoc cref="global::System.IO.Path.GetDirectoryName(global::System.String)"/>
public global::System.String GetDirectoryName(global::System.String path)
{
return global::System.IO.Path.GetDirectoryName(path);
}
/// <inheritdoc cref="global::System.IO.Path.GetExtension(global::System.ReadOnlySpan<global::System.Char>)"/>
public global::System.ReadOnlySpan<global::System.Char> GetExtension(global::System.ReadOnlySpan<global::System.Char> path)
{
return global::System.IO.Path.GetExtension(path);
}
/// <inheritdoc cref="global::System.IO.Path.GetExtension(global::System.String)"/>
public global::System.String GetExtension(global::System.String path)
{
return global::System.IO.Path.GetExtension(path);
}
/// <inheritdoc cref="global::System.IO.Path.GetFileName(global::System.ReadOnlySpan<global::System.Char>)"/>
public global::System.ReadOnlySpan<global::System.Char> GetFileName(global::System.ReadOnlySpan<global::System.Char> path)
{
return global::System.IO.Path.GetFileName(path);
}
/// <inheritdoc cref="global::System.IO.Path.GetFileName(global::System.String)"/>
public global::System.String GetFileName(global::System.String path)
{
return global::System.IO.Path.GetFileName(path);
}
/// <inheritdoc cref="global::System.IO.Path.GetFileNameWithoutExtension(global::System.ReadOnlySpan<global::System.Char>)"/>
public global::System.ReadOnlySpan<global::System.Char> GetFileNameWithoutExtension(global::System.ReadOnlySpan<global::System.Char> path)
{
return global::System.IO.Path.GetFileNameWithoutExtension(path);
}
/// <inheritdoc cref="global::System.IO.Path.GetFileNameWithoutExtension(global::System.String)"/>
public global::System.String GetFileNameWithoutExtension(global::System.String path)
{
return global::System.IO.Path.GetFileNameWithoutExtension(path);
}
/// <inheritdoc cref="global::System.IO.Path.GetFullPath(global::System.String)"/>
public virtual global::System.String GetFullPath(global::System.String path)
{
throw new global::System.NotSupportedException();
}
/// <inheritdoc cref="global::System.IO.Path.GetRelativePath(global::System.String, global::System.String)"/>
public global::System.String GetRelativePath(global::System.String relativeTo, global::System.String path)
{
return global::System.IO.Path.GetRelativePath(relativeTo, path);
}
/// <inheritdoc cref="global::System.IO.Path.IsPathRooted(global::System.ReadOnlySpan<global::System.Char>)"/>
public virtual global::System.Boolean IsPathRooted(global::System.ReadOnlySpan<global::System.Char> path)
{
throw new global::System.NotSupportedException();

View File

@ -481,5 +481,25 @@ public partial class VirtualFileSystem : FileSystem
string fullPath = GetFullPath(path);
return fullPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
}
public override string Join(string path1, string path2)
{
return GetFullPath(base.Join(path1, path2));
}
public override string Join(string path1, string path2, string path3)
{
return GetFullPath(base.Join(path1, path2, path3));
}
public override string Join(string path1, string path2, string path3, string path4)
{
return GetFullPath(base.Join(path1, path2, path3, path4));
}
public override string Join(params ReadOnlySpan<string> paths)
{
return GetFullPath(base.Join(paths));
}
}
}

View File

@ -50,7 +50,7 @@ public sealed class GameStructure : IDisposable
public static GameStructure Load(IEnumerable<string> paths, FileSystem fileSystem, CoreConfiguration configuration)
{
List<string> toProcess = ZipExtractor.Process(paths);
List<string> toProcess = ZipExtractor.Process(paths, fileSystem);
if (toProcess.Count == 0)
{
throw new ArgumentException("Game files not found", nameof(paths));

View File

@ -1,11 +1,12 @@
using AssetRipper.Import.Logging;
using AssetRipper.IO.Files;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace AssetRipper.Import.Structure;
public static class ZipExtractor
internal static class ZipExtractor
{
private const string ZipExtension = ".zip";
private const string ApkExtension = ".apk";
@ -19,24 +20,24 @@ public static class ZipExtractor
private const uint ZipEmptyMagic = 0x06054B50;
private const uint ZipSpannedMagic = 0x08074B50;
public static List<string> Process(IEnumerable<string> paths)
public static List<string> Process(IEnumerable<string> paths, FileSystem fileSystem)
{
List<string> result = new();
List<string> result = [];
foreach (string path in paths)
{
switch (GetFileExtension(path))
switch (GetFileExtension(path, fileSystem))
{
case ZipExtension:
case ApkExtension:
case ObbExtension:
case VpkExtension:
case IpaExtension:
result.Add(ExtractZip(path));
result.Add(ExtractZip(path, fileSystem));
break;
case ApksExtension:
case ApkPlusExtension:
case XapkExtension:
result.Add(ExtractXapk(path));
result.Add(ExtractXapk(path, fileSystem));
break;
default:
result.Add(path);
@ -46,55 +47,101 @@ public static class ZipExtractor
return result;
}
private static string ExtractZip(string zipFilePath)
private static string ExtractZip(string zipFilePath, FileSystem fileSystem)
{
if (!HasCompatibleMagic(zipFilePath))
if (!HasCompatibleMagic(zipFilePath, fileSystem))
{
return zipFilePath;
}
string outputDirectory = LocalFileSystem.Instance.Directory.CreateTemporary();
DecompressZipArchive(zipFilePath, outputDirectory);
string outputDirectory = fileSystem.Directory.CreateTemporary();
DecompressZipArchive(zipFilePath, outputDirectory, fileSystem);
return outputDirectory;
}
private static string ExtractXapk(string xapkFilePath)
private static string ExtractXapk(string xapkFilePath, FileSystem fileSystem)
{
if (!HasCompatibleMagic(xapkFilePath))
if (!HasCompatibleMagic(xapkFilePath, fileSystem))
{
return xapkFilePath;
}
string intermediateDirectory = LocalFileSystem.Instance.Directory.CreateTemporary();
string outputDirectory = LocalFileSystem.Instance.Directory.CreateTemporary();
DecompressZipArchive(xapkFilePath, intermediateDirectory);
foreach (string filePath in Directory.GetFiles(intermediateDirectory))
string intermediateDirectory = fileSystem.Directory.CreateTemporary();
string outputDirectory = fileSystem.Directory.CreateTemporary();
DecompressZipArchive(xapkFilePath, intermediateDirectory, fileSystem);
foreach (string filePath in fileSystem.Directory.GetFiles(intermediateDirectory))
{
if (GetFileExtension(filePath) == ApkExtension)
if (GetFileExtension(filePath, fileSystem) == ApkExtension)
{
DecompressZipArchive(filePath, outputDirectory);
DecompressZipArchive(filePath, outputDirectory, fileSystem);
}
}
return outputDirectory;
}
private static void DecompressZipArchive(string zipFilePath, string outputDirectory)
private static void DecompressZipArchive(string zipFilePath, string outputDirectory, FileSystem fileSystem)
{
Logger.Info(LogCategory.Import, $"Decompressing files...{Environment.NewLine}\tFrom: {zipFilePath}{Environment.NewLine}\tTo: {outputDirectory}");
using ZipArchive archive = ZipArchive.Open(zipFilePath);
using Stream stream = fileSystem.File.OpenRead(zipFilePath);
using ZipArchive archive = ZipArchive.Open(stream);
using IReader reader = archive.ExtractAllEntries();
reader.WriteAllToDirectory(outputDirectory, new SharpCompress.Common.ExtractionOptions()
while (reader.MoveToNextEntry())
{
ExtractFullPath = true,
Overwrite = true
});
WriteEntryToDirectory(reader, outputDirectory, fileSystem);
}
}
private static string? GetFileExtension(string path)
private static void WriteEntryToDirectory(IReader reader, string outputDirectory, FileSystem fileSystem)
{
if (File.Exists(path))
IEntry entry = reader.Entry;
string filePath;
string fullOutputDirectory = fileSystem.Path.GetFullPath(outputDirectory);
if (!fileSystem.Directory.Exists(fullOutputDirectory))
{
return Path.GetExtension(path);
throw new ExtractionException($"Directory does not exist to extract to: {fullOutputDirectory}");
}
string fileName = fileSystem.Path.GetFileName(entry.Key ?? throw new NullReferenceException("Entry Key is null")) ?? throw new NullReferenceException("File is null");
fileName = FileSystem.FixInvalidFileNameCharacters(fileName);
string directory = fileSystem.Path.GetDirectoryName(entry.Key ?? throw new NullReferenceException("Entry Key is null")) ?? throw new NullReferenceException("Directory is null");
string fullDirectory = fileSystem.Path.GetFullPath(fileSystem.Path.Join(fullOutputDirectory, directory));
if (!fileSystem.Directory.Exists(fullDirectory))
{
if (!fullDirectory.StartsWith(fullOutputDirectory, StringComparison.Ordinal))
{
throw new ExtractionException("Entry is trying to create a directory outside of the destination directory.");
}
fileSystem.Directory.Create(fullDirectory);
}
filePath = fileSystem.Path.Join(fullDirectory, fileName);
if (!entry.IsDirectory)
{
filePath = fileSystem.Path.GetFullPath(filePath);
if (!filePath.StartsWith(fullOutputDirectory,StringComparison.Ordinal))
{
throw new ExtractionException("Entry is trying to write a file outside of the destination directory.");
}
using Stream stream = fileSystem.File.Create(filePath);
reader.WriteEntryTo(stream);
}
else if (!fileSystem.Directory.Exists(filePath))
{
fileSystem.Directory.Create(filePath);
}
}
private static string? GetFileExtension(string path, FileSystem fileSystem)
{
if (fileSystem.File.Exists(path))
{
return fileSystem.Path.GetExtension(path);
}
else
{
@ -102,14 +149,15 @@ public static class ZipExtractor
}
}
private static bool HasCompatibleMagic(string path)
private static bool HasCompatibleMagic(string path, FileSystem fileSystem)
{
uint magic = GetMagicNumber(path);
uint magic = GetMagicNumber(path, fileSystem);
return magic == ZipNormalMagic || magic == ZipEmptyMagic || magic == ZipSpannedMagic;
}
private static uint GetMagicNumber(string path)
private static uint GetMagicNumber(string path, FileSystem fileSystem)
{
return new BinaryReader(File.OpenRead(path)).ReadUInt32();
using Stream stream = fileSystem.File.OpenRead(path);
return new BinaryReader(stream).ReadUInt32();
}
}