diff --git a/Source/AssetRipper.IO.Files.SourceGenerator/ParameterExtensions.cs b/Source/AssetRipper.IO.Files.SourceGenerator/ParameterExtensions.cs new file mode 100644 index 000000000..895ec4cc3 --- /dev/null +++ b/Source/AssetRipper.IO.Files.SourceGenerator/ParameterExtensions.cs @@ -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() is not null; + } + + public static string GetParamsPrefix(this ParameterInfo parameter) + { + return parameter.IsParams() ? "params " : ""; + } +} diff --git a/Source/AssetRipper.IO.Files.SourceGenerator/Program.cs b/Source/AssetRipper.IO.Files.SourceGenerator/Program.cs index cc6d3ba4a..38243e1b5 100644 --- a/Source/AssetRipper.IO.Files.SourceGenerator/Program.cs +++ b/Source/AssetRipper.IO.Files.SourceGenerator/Program.cs @@ -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(); } + /// 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($"/// "); + 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> apiDictionary = new() + private static readonly Dictionary> apiDictionary = new() { - [nameof(File)] = new() - { + [nameof(File)] = + [ new((Func)File.Create), new(File.Delete), new(File.Exists), @@ -185,9 +184,9 @@ internal static class Program new((Action>)File.WriteAllBytes), new((Action>)File.WriteAllText), new((Action, Encoding>)File.WriteAllText), - }, - [nameof(Directory)] = new() - { + ], + [nameof(Directory)] = + [ new((Func>)Directory.EnumerateDirectories), new((Func>)Directory.EnumerateFiles), new((Func>)Directory.GetDirectories), @@ -201,9 +200,9 @@ internal static class Program new((Func>)Directory.GetDirectories), new((Func>)Directory.GetFiles), new(Directory.Exists), - }, - [nameof(Path)] = new() - { + ], + [nameof(Path)] = + [ new((Func)Path.Join) { Type = FileSystemApiType.Virtual }, new((Func)Path.Join) { Type = FileSystemApiType.Virtual }, new((Func)Path.Join) { Type = FileSystemApiType.Virtual }, @@ -219,7 +218,7 @@ internal static class Program new((Func)Path.GetFullPath), new(Path.GetRelativePath) { Type = FileSystemApiType.Sealed }, new((Func, 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() 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}"; - } - } -} diff --git a/Source/AssetRipper.IO.Files.SourceGenerator/TypeExtensions.cs b/Source/AssetRipper.IO.Files.SourceGenerator/TypeExtensions.cs new file mode 100644 index 000000000..d653bf2eb --- /dev/null +++ b/Source/AssetRipper.IO.Files.SourceGenerator/TypeExtensions.cs @@ -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}"; + } + } +} diff --git a/Source/AssetRipper.IO.Files/FileSystem.g.cs b/Source/AssetRipper.IO.Files/FileSystem.g.cs index 3810290bb..45e22e509 100644 --- a/Source/AssetRipper.IO.Files/FileSystem.g.cs +++ b/Source/AssetRipper.IO.Files/FileSystem.g.cs @@ -12,56 +12,67 @@ public abstract partial class FileSystem protected FileImplementation File => this; protected DirectoryImplementation Directory => Parent.Directory; protected PathImplementation Path => Parent.Path; + /// public virtual global::System.IO.Stream Create(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual void Delete(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.Boolean Exists(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.IO.Stream OpenRead(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.IO.Stream OpenWrite(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.Byte[] ReadAllBytes(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.String ReadAllText(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.String ReadAllText(global::System.String path, global::System.Text.Encoding encoding) { throw new global::System.NotSupportedException(); } + /// public virtual void WriteAllBytes(global::System.String path, global::System.ReadOnlySpan bytes) { throw new global::System.NotSupportedException(); } + /// public virtual void WriteAllText(global::System.String path, global::System.ReadOnlySpan contents) { throw new global::System.NotSupportedException(); } + /// public virtual void WriteAllText(global::System.String path, global::System.ReadOnlySpan 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; + /// public virtual global::System.Collections.Generic.IEnumerable EnumerateDirectories(global::System.String path, global::System.String searchPattern, global::System.IO.SearchOption searchOption) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.Collections.Generic.IEnumerable EnumerateFiles(global::System.String path, global::System.String searchPattern, global::System.IO.SearchOption searchOption) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.String[] GetDirectories(global::System.String path, global::System.String searchPattern, global::System.IO.SearchOption searchOption) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.String[] GetFiles(global::System.String path, global::System.String searchPattern, global::System.IO.SearchOption searchOption) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.Collections.Generic.IEnumerable EnumerateDirectories(global::System.String path, global::System.String searchPattern) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.Collections.Generic.IEnumerable EnumerateFiles(global::System.String path, global::System.String searchPattern) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.String[] GetDirectories(global::System.String path, global::System.String searchPattern) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.String[] GetFiles(global::System.String path, global::System.String searchPattern) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.Collections.Generic.IEnumerable EnumerateDirectories(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.Collections.Generic.IEnumerable EnumerateFiles(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.String[] GetDirectories(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public virtual global::System.String[] GetFiles(global::System.String path) { throw new global::System.NotSupportedException(); } + /// 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; + /// public virtual global::System.String Join(global::System.String path1, global::System.String path2) { return global::System.IO.Path.Join(path1, path2); } + /// 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); } + /// 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); } + /// public virtual global::System.String Join(params global::System.ReadOnlySpan paths) { return global::System.IO.Path.Join(paths); } + /// public global::System.ReadOnlySpan GetDirectoryName(global::System.ReadOnlySpan path) { return global::System.IO.Path.GetDirectoryName(path); } + /// public global::System.String GetDirectoryName(global::System.String path) { return global::System.IO.Path.GetDirectoryName(path); } + /// public global::System.ReadOnlySpan GetExtension(global::System.ReadOnlySpan path) { return global::System.IO.Path.GetExtension(path); } + /// public global::System.String GetExtension(global::System.String path) { return global::System.IO.Path.GetExtension(path); } + /// public global::System.ReadOnlySpan GetFileName(global::System.ReadOnlySpan path) { return global::System.IO.Path.GetFileName(path); } + /// public global::System.String GetFileName(global::System.String path) { return global::System.IO.Path.GetFileName(path); } + /// public global::System.ReadOnlySpan GetFileNameWithoutExtension(global::System.ReadOnlySpan path) { return global::System.IO.Path.GetFileNameWithoutExtension(path); } + /// public global::System.String GetFileNameWithoutExtension(global::System.String path) { return global::System.IO.Path.GetFileNameWithoutExtension(path); } + /// public virtual global::System.String GetFullPath(global::System.String path) { throw new global::System.NotSupportedException(); } + /// public global::System.String GetRelativePath(global::System.String relativeTo, global::System.String path) { return global::System.IO.Path.GetRelativePath(relativeTo, path); } + /// public virtual global::System.Boolean IsPathRooted(global::System.ReadOnlySpan path) { throw new global::System.NotSupportedException(); diff --git a/Source/AssetRipper.IO.Files/VirtualFileSystem.cs b/Source/AssetRipper.IO.Files/VirtualFileSystem.cs index 2308047ec..057846dc5 100644 --- a/Source/AssetRipper.IO.Files/VirtualFileSystem.cs +++ b/Source/AssetRipper.IO.Files/VirtualFileSystem.cs @@ -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 paths) + { + return GetFullPath(base.Join(paths)); + } } } diff --git a/Source/AssetRipper.Import/Structure/GameStructure.cs b/Source/AssetRipper.Import/Structure/GameStructure.cs index 8c2af0615..8a8d327b9 100644 --- a/Source/AssetRipper.Import/Structure/GameStructure.cs +++ b/Source/AssetRipper.Import/Structure/GameStructure.cs @@ -50,7 +50,7 @@ public sealed class GameStructure : IDisposable public static GameStructure Load(IEnumerable paths, FileSystem fileSystem, CoreConfiguration configuration) { - List toProcess = ZipExtractor.Process(paths); + List toProcess = ZipExtractor.Process(paths, fileSystem); if (toProcess.Count == 0) { throw new ArgumentException("Game files not found", nameof(paths)); diff --git a/Source/AssetRipper.Import/Structure/ZipExtractor.cs b/Source/AssetRipper.Import/Structure/ZipExtractor.cs index f2a498234..85d9e5d6f 100644 --- a/Source/AssetRipper.Import/Structure/ZipExtractor.cs +++ b/Source/AssetRipper.Import/Structure/ZipExtractor.cs @@ -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 Process(IEnumerable paths) + public static List Process(IEnumerable paths, FileSystem fileSystem) { - List result = new(); + List 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(); } }