diff --git a/Mara.Generator/Mara.Generator.csproj b/Mara.Generator/Mara.Generator.csproj index 8b0ca65..18c2438 100644 --- a/Mara.Generator/Mara.Generator.csproj +++ b/Mara.Generator/Mara.Generator.csproj @@ -2,7 +2,7 @@ WinExe - net5.0-windows + net6.0-windows true diff --git a/Mara.Lib/Common/Xdelta.cs b/Mara.Lib/Common/Xdelta.cs index bf36abe..77bd7fa 100644 --- a/Mara.Lib/Common/Xdelta.cs +++ b/Mara.Lib/Common/Xdelta.cs @@ -1,5 +1,5 @@ using System.IO; -using Pleosoft.XdeltaSharp; +using PleOps.XdeltaSharp.Decoder; namespace Mara.Lib.Common { @@ -15,5 +15,18 @@ public static void Apply(FileStream file, byte[] xdeltaFile, string outfile) outStream.Close(); file.Close(); } + + public static void ApplyFileStream(string file, string xdeltaFile, string outfile) + { + using var fileStream = new FileStream(file, FileMode.Open); + using var xdeltaStream = new FileStream(xdeltaFile, FileMode.Open); + using var outStream = new FileStream(outfile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + + var decoder = new Decoder(fileStream, xdeltaStream, outStream); + decoder.Run(); + fileStream.Close(); + xdeltaStream.Close(); + outStream.Close(); + } } } diff --git a/Mara.Lib/Dll/Pleosoft.XdeltaSharp.deps.json b/Mara.Lib/Dll/Pleosoft.XdeltaSharp.deps.json deleted file mode 100644 index 8413375..0000000 --- a/Mara.Lib/Dll/Pleosoft.XdeltaSharp.deps.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETStandard,Version=v2.0/", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETStandard,Version=v2.0": {}, - ".NETStandard,Version=v2.0/": { - "Pleosoft.XdeltaSharp/1.0.0": { - "dependencies": { - "Microsoft.SourceLink.GitHub": "1.0.0", - "NETStandard.Library": "2.0.3", - "Roslynator.Analyzers": "3.0.0", - "SonarAnalyzer.CSharp": "8.15.0.24505", - "StyleCop.Analyzers": "1.1.118", - "System.Buffers": "4.5.1" - }, - "runtime": { - "Pleosoft.XdeltaSharp.dll": {} - } - }, - "Microsoft.Build.Tasks.Git/1.0.0": {}, - "Microsoft.NETCore.Platforms/1.1.0": {}, - "Microsoft.SourceLink.Common/1.0.0": {}, - "Microsoft.SourceLink.GitHub/1.0.0": { - "dependencies": { - "Microsoft.Build.Tasks.Git": "1.0.0", - "Microsoft.SourceLink.Common": "1.0.0" - } - }, - "NETStandard.Library/2.0.3": { - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - } - }, - "Roslynator.Analyzers/3.0.0": {}, - "SonarAnalyzer.CSharp/8.15.0.24505": {}, - "StyleCop.Analyzers/1.1.118": {}, - "System.Buffers/4.5.1": { - "runtime": { - "lib/netstandard2.0/System.Buffers.dll": { - "assemblyVersion": "4.0.3.0", - "fileVersion": "4.6.28619.1" - } - } - } - } - }, - "libraries": { - "Pleosoft.XdeltaSharp/1.0.0": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "Microsoft.Build.Tasks.Git/1.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-z2fpmmt+1Jfl+ZnBki9nSP08S1/tbEOxFdsK1rSR+LBehIJz1Xv9/6qOOoGNqlwnAGGVGis1Oj6S8Kt9COEYlQ==", - "path": "microsoft.build.tasks.git/1.0.0", - "hashPath": "microsoft.build.tasks.git.1.0.0.nupkg.sha512" - }, - "Microsoft.NETCore.Platforms/1.1.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==", - "path": "microsoft.netcore.platforms/1.1.0", - "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512" - }, - "Microsoft.SourceLink.Common/1.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-G8DuQY8/DK5NN+3jm5wcMcd9QYD90UV7MiLmdljSJixi3U/vNaeBKmmXUqI4DJCOeWizIUEh4ALhSt58mR+5eg==", - "path": "microsoft.sourcelink.common/1.0.0", - "hashPath": "microsoft.sourcelink.common.1.0.0.nupkg.sha512" - }, - "Microsoft.SourceLink.GitHub/1.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-aZyGyGg2nFSxix+xMkPmlmZSsnGQ3w+mIG23LTxJZHN+GPwTQ5FpPgDo7RMOq+Kcf5D4hFWfXkGhoGstawX13Q==", - "path": "microsoft.sourcelink.github/1.0.0", - "hashPath": "microsoft.sourcelink.github.1.0.0.nupkg.sha512" - }, - "NETStandard.Library/2.0.3": { - "type": "package", - "serviceable": true, - "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", - "path": "netstandard.library/2.0.3", - "hashPath": "netstandard.library.2.0.3.nupkg.sha512" - }, - "Roslynator.Analyzers/3.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-UKaYNtbXGGkR83rMA42DbA43fIUebO+WfHvPk29WMZfXzzF9kTKEB0HL6dUmcukgeHwXyTBCj6946DSaoow02A==", - "path": "roslynator.analyzers/3.0.0", - "hashPath": "roslynator.analyzers.3.0.0.nupkg.sha512" - }, - "SonarAnalyzer.CSharp/8.15.0.24505": { - "type": "package", - "serviceable": true, - "sha512": "sha512-r4Pt4c/FV1m//NuvVP5k0PmGBceJTyV9hv0sGXSd2bpLb2I0yX3IShuGcCekkS2E82Gay5feKlfOK9b+G3vr1Q==", - "path": "sonaranalyzer.csharp/8.15.0.24505", - "hashPath": "sonaranalyzer.csharp.8.15.0.24505.nupkg.sha512" - }, - "StyleCop.Analyzers/1.1.118": { - "type": "package", - "serviceable": true, - "sha512": "sha512-Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==", - "path": "stylecop.analyzers/1.1.118", - "hashPath": "stylecop.analyzers.1.1.118.nupkg.sha512" - }, - "System.Buffers/4.5.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==", - "path": "system.buffers/4.5.1", - "hashPath": "system.buffers.4.5.1.nupkg.sha512" - } - } -} \ No newline at end of file diff --git a/Mara.Lib/Dll/Pleosoft.XdeltaSharp.dll b/Mara.Lib/Dll/Pleosoft.XdeltaSharp.dll deleted file mode 100644 index f4e101e..0000000 Binary files a/Mara.Lib/Dll/Pleosoft.XdeltaSharp.dll and /dev/null differ diff --git a/Mara.Lib/Dll/Pleosoft.XdeltaSharp.xml b/Mara.Lib/Dll/Pleosoft.XdeltaSharp.xml deleted file mode 100644 index 6bfb571..0000000 --- a/Mara.Lib/Dll/Pleosoft.XdeltaSharp.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - Pleosoft.XdeltaSharp - - - - - Adler-32 checksum algorithm. - - - For more information: https://en.wikipedia.org/wiki/Adler-32. - - - - - Computes the checksum Adler-32 from a stream. - - The stream to read data. It starts at the current position. - The amount of bytes to read from the stream for the checksum. - The start value of the checksum. - The Adler-32 checksum of the data. - - - - Decodes an integer of variable length. - Defined in RCF-3284, the VCDIFF format uses a variable-length integer. - Each byte contains 7-bits of data and the last bit indicates if there - more bytes to decode. - - The integer. - - - diff --git a/Mara.Lib/Mara.Lib.csproj b/Mara.Lib/Mara.Lib.csproj index e1cc009..4ea488d 100644 --- a/Mara.Lib/Mara.Lib.csproj +++ b/Mara.Lib/Mara.Lib.csproj @@ -1,14 +1,15 @@ - net5.0 + net6.0 - + + - + @@ -19,9 +20,6 @@ Dll\LibHac.dll - - Dll\Pleosoft.XdeltaSharp.dll - diff --git a/Mara.Lib/Platforms/3ds/Main.cs b/Mara.Lib/Platforms/3ds/Main.cs index 593acdc..a88e12c 100644 --- a/Mara.Lib/Platforms/3ds/Main.cs +++ b/Mara.Lib/Platforms/3ds/Main.cs @@ -1,12 +1,246 @@ -using System; +using SceneGate.Lemon.Containers.Converters; +using SceneGate.Lemon.Titles; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; namespace Mara.Lib.Platforms._3ds { - class Main + public class Main : PatchProcess { + public PatchMode PatchMode { get; set; } + bool UseFileStream { get; set; } + Node Cia { get; set; } + Node ContentNode { get; set; } + Dictionary> ContentToPatch = new Dictionary>(); + + public Main(string oriFile, string outFile, string patchPath, PatchMode patchMode, bool useFileStream = false) : base(oriFile, outFile, patchPath) + { + PatchMode = patchMode; + UseFileStream = useFileStream; + + try + { + Cia = NodeFactory.FromFile(oriFile, "root").TransformWith(); + } + catch (Exception ex) + { + if (ex is FormatException) + { + throw new FormatException($"{Path.GetFileName(oriFile)} is an invalid CIA file.", ex); + } + else + { + throw new Exception($"An error has ocurred while reading the file.", ex); + } + } + + ContentNode = Cia.Children["content"]; + + foreach (var content in ContentNode.Children) + { + if (content.Tags.ContainsKey("LEMON_NCCH_ENCRYPTED")) + { + throw new Exception("Encrypted (legit) CIA not supported"); + } + + for (int i = 0; i < maraConfig.FilesInfo.ListOriFiles.Length; i++) + { + var splitLine = maraConfig.FilesInfo.ListOriFiles[i].Split('\\'); + + if (splitLine[0] == content.Name) + { + if (ContentToPatch.ContainsKey(splitLine[0])) + { + if (!ContentToPatch[splitLine[0]].Contains(splitLine[1])) + { + ContentToPatch[splitLine[0]].Add(splitLine[1]); + } + } + else + { + ContentToPatch.Add(splitLine[0], new List { splitLine[1] }); + } + + if (patchMode == PatchMode.Specific) + { + switch(splitLine[1]) + { + case "rom": + break; + case "system": + break; + default: + throw new FormatException($"Unsupported specific patching for {splitLine[1]}"); + } + } + } + } + } + } + + public override (int, string) ApplyTranslation() + { + var count = maraConfig.FilesInfo.ListOriFiles.Length; + var files = maraConfig.FilesInfo; + + switch (PatchMode) + { + case PatchMode.General: + foreach (var contentToPatch in ContentToPatch) + { + var contentChildNode = ContentNode.Children[contentToPatch.Key]; + contentChildNode.TransformWith(); + + foreach (var node in Navigator.IterateNodes(contentChildNode)) + { + node.Stream.WriteTo(tempFolder + Path.DirectorySeparatorChar + contentToPatch.Key + Path.DirectorySeparatorChar + node.Name); + } + } + + for (int i = 0; i < count; i++) + { + var file = $"{tempFolder}{Path.DirectorySeparatorChar}{files.ListOriFiles[i]}"; + var xdelta = $"{tempFolder}{Path.DirectorySeparatorChar}{files.ListXdeltaFiles[i]}"; + + var result = ApplyXdelta(file, xdelta, file, files.ListMd5Files[i], UseFileStream); + + if (result.Item1 != 0) + return result; + + var content = files.ListOriFiles[i].Split('\\')[0]; + ContentNode.Children[content].Add(NodeFactory.FromFile(file, Path.GetFileName(files.ListOriFiles[i]))); + } + + foreach (var contentToPatch in ContentToPatch) + { + var contentChildNode = ContentNode.Children[contentToPatch.Key]; + var contentChildPath = $"{tempFolder}{Path.DirectorySeparatorChar}{contentToPatch.Key}"; + contentChildNode.TransformWith(); + + // Doing this reduces the amount of memory used. + Directory.Delete(contentChildPath, true); + contentChildNode.Stream.WriteTo(contentChildPath); + + ContentNode.Add(NodeFactory.FromFile(contentChildPath, contentToPatch.Key)); + } + break; + case PatchMode.Specific: + foreach (var contentToPatch in ContentToPatch) + { + var contentNode = ContentNode.Children[contentToPatch.Key]; + contentNode.TransformWith(); + + foreach (var node in Navigator.IterateNodes(contentNode)) + { + if (contentToPatch.Value.Contains(node.Name)) + { + switch (node.Name) + { + case "rom": + node.TransformWith(); + break; + case "system": + node.TransformWith(); + break; + default: + continue; + } + + foreach (var nodeChild in Navigator.IterateNodes(node)) + { + var path = nodeChild.Path.Replace($"/root/content/{contentToPatch.Key}/", "").Replace('/', Path.DirectorySeparatorChar); + + // Using node.Stream will return null for some reason + var nodeChildFromRoot = Navigator.SearchNode(Cia, nodeChild.Path); + if (!nodeChildFromRoot.IsContainer) + { + nodeChildFromRoot.Stream.WriteTo(tempFolder + Path.DirectorySeparatorChar + contentToPatch.Key + Path.DirectorySeparatorChar + path); + } + } + } + } + } + + for (int i = 0; i < count; i++) + { + var file = $"{tempFolder}{Path.DirectorySeparatorChar}{files.ListOriFiles[i]}"; + var xdelta = $"{tempFolder}{Path.DirectorySeparatorChar}{files.ListXdeltaFiles[i]}"; + + var result = ApplyXdelta(file, xdelta, file, files.ListMd5Files[i], UseFileStream); + + if (result.Item1 != 0) + return result; + + // We need to delete these since later we're going to make a new Node container with this folder + if (File.Exists($"{file}_ori")) + { + File.Delete($"{file}_ori"); + } + + if (File.Exists(xdelta)) + { + File.Delete(xdelta); + } + } + + foreach (var contentToPatch in ContentToPatch) + { + var contentPath = $"{tempFolder}{Path.DirectorySeparatorChar}{contentToPatch.Key}"; + + foreach (var contentChild in ContentToPatch[contentToPatch.Key]) + { + var contentChildPath = $"{contentPath}{Path.DirectorySeparatorChar}{contentChild}"; + ContentNode.Children[contentToPatch.Key].Add(NodeFactory.FromDirectory(contentChildPath, "*", contentChild, true)); + + switch (contentChild) + { + case "rom": + ContentNode.Children[contentToPatch.Key].Children[contentChild].TransformWith(); + break; + case "system": + IConverter binaryConverter = new BinaryExeFs2NodeContainer(); + var convertedNode = binaryConverter.Convert(ContentNode.Children[contentToPatch.Key].Children[contentChild].GetFormatAs()); + ContentNode.Children[contentToPatch.Key].Children[contentChild].ChangeFormat(convertedNode); + break; + } + } + + ContentNode.Children[contentToPatch.Key].TransformWith(); + + // Doing this reduces the amount of memory used. + Directory.Delete(contentPath, true); + ContentNode.Children[contentToPatch.Key].Stream.WriteTo(contentPath); + ContentNode.Children[contentToPatch.Key].Dispose(); + + ContentNode.Add(NodeFactory.FromFile(contentPath, contentToPatch.Key)); + } + break; + } + + Cia.TransformWith(); + + if (File.Exists(outFolder)) + { + File.Delete(outFolder); + } + + Cia.Stream.WriteTo(outFolder); + Cia.Stream?.Dispose(); + + return base.ApplyTranslation(); + } + } + + public enum PatchMode + { + General, + Specific } } diff --git a/Mara.Lib/Platforms/PatchProcess.cs b/Mara.Lib/Platforms/PatchProcess.cs index 5fa00e6..7a14faa 100644 --- a/Mara.Lib/Platforms/PatchProcess.cs +++ b/Mara.Lib/Platforms/PatchProcess.cs @@ -30,7 +30,7 @@ public virtual (int, string) ApplyTranslation() } - protected (int, string) ApplyXdelta(string file, string xdelta, string result, string md5) + protected (int, string) ApplyXdelta(string file, string xdelta, string result, string md5, bool deltaFileStream = false) { if (!File.Exists(file + "_ori")) File.Move(file, file + "_ori"); @@ -50,7 +50,10 @@ public virtual (int, string) ApplyTranslation() try { - Common.Xdelta.Apply(File.Open(file, FileMode.Open), File.ReadAllBytes(xdelta), result); + if (!deltaFileStream) + Common.Xdelta.Apply(File.Open(file, FileMode.Open), File.ReadAllBytes(xdelta), result); + else + Common.Xdelta.ApplyFileStream(file, xdelta, result); } catch (Exception e) { diff --git a/Mara.Patcher/Mara.Patcher.csproj b/Mara.Patcher/Mara.Patcher.csproj index 80c7c77..eaee386 100644 --- a/Mara.Patcher/Mara.Patcher.csproj +++ b/Mara.Patcher/Mara.Patcher.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 diff --git a/Mara.Tester/Mara.Tester.csproj b/Mara.Tester/Mara.Tester.csproj index 40d934e..4b60bcb 100644 --- a/Mara.Tester/Mara.Tester.csproj +++ b/Mara.Tester/Mara.Tester.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6.0 diff --git a/Mara.Tester/Program.cs b/Mara.Tester/Program.cs index 9b69850..4db6cc1 100644 --- a/Mara.Tester/Program.cs +++ b/Mara.Tester/Program.cs @@ -11,17 +11,7 @@ static void Main(string[] args) if (args.Length != 4) PrintInfo(); - // Check ori folder - if (!CheckDirectoryOrFile(args[1], true)) - return; - - // Check result folder - if (!CheckDirectoryOrFile(args[2], true)) - return; - - // Check zip file - if (!CheckDirectoryOrFile(args[3], false)) - return; + CheckPaths(args[1], args[2], args[3], args[0]); switch (args[0].ToUpper()) { @@ -31,6 +21,12 @@ static void Main(string[] args) case "PSVITA": ImportVita(args[1], args[2], args[3]); break; + case "3DS-GENERAL": + Import3dsGeneral(args[1], args[2], args[3]); + break; + case "3DS-SPECIFIC": + Import3dsSpecific(args[1], args[2], args[3]); + break; default: PrintInfo(); break; @@ -50,6 +46,18 @@ private static void ImportPc(string oriFolder, string outFolder, string zipPatch PrintResult(mainVita.ApplyTranslation()); } + private static void Import3dsGeneral(string oriFile, string outFile, string zipPatch) + { + var main3DS = new Lib.Platforms._3ds.Main(oriFile, outFile, zipPatch, Lib.Platforms._3ds.PatchMode.General); + PrintResult(main3DS.ApplyTranslation()); + } + + private static void Import3dsSpecific(string oriFile, string outFile, string zipPatch) + { + var main3DS = new Lib.Platforms._3ds.Main(oriFile, outFile, zipPatch, Lib.Platforms._3ds.PatchMode.Specific); + PrintResult(main3DS.ApplyTranslation()); + } + private static void PrintResult((int, string) result) { Console.WriteLine($"RESULT CODE: {result.Item1}\n" + @@ -64,9 +72,38 @@ private static void PrintInfo() Console.WriteLine("Mara.Tester PSVITA \"ORIGINAL FILES FOLDER\" \"OUT PATCHED FILES FOLDER\" \"ZIP DATA PATH\""); } - private static bool CheckDirectoryOrFile(string path, bool isDirectory) + static void CheckPaths(string oriPath, string outPath, string zipPath, string patchPlatform) { - if (!isDirectory) + bool isDirectory = true; + + switch (patchPlatform.ToUpper()) + { + case "3DS-GENERAL": + isDirectory = false; + outPath = Path.GetDirectoryName(outPath); + break; + case "3DS-SPECIFIC": + isDirectory = false; + outPath = Path.GetDirectoryName(outPath); + break; + } + + // Check ori folder/file + if (!CheckDirectoryOrFile(oriPath, isDirectory)) + return; + + // Check result folder + if (!CheckDirectoryOrFile(outPath, true, true)) + return; + + // Check zip file + if (!CheckDirectoryOrFile(zipPath, false)) + return; + } + + private static bool CheckDirectoryOrFile(string path, bool isDirectory, bool isOut = false) + { + if (!isDirectory && !isOut) { if (File.Exists(path)) return true; @@ -75,6 +112,10 @@ private static bool CheckDirectoryOrFile(string path, bool isDirectory) return false; } + else if (!isDirectory && isOut) + { + path = Path.GetDirectoryName(path); + } if (Directory.Exists(path)) return true;