diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..17c1deef --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + pull_request: + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + - name: Run publish for net8.0 + shell: pwsh + run: | + pwsh -NoProfile -ExecutionPolicy Bypass -File .\build-ci.ps1 -TargetFrameworks 'net8.0' + + - name: Setup .NET 9 (optional) + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '9.0.x' + - name: Run publish for net9.0 (optional) + shell: pwsh + run: | + pwsh -NoProfile -ExecutionPolicy Bypass -File .\build-ci.ps1 -TargetFrameworks 'net9.0' -Rids 'win-x64' diff --git a/Il2CppDumper.sln b/Il2CppDumper.sln index 26658714..ea21670f 100644 --- a/Il2CppDumper.sln +++ b/Il2CppDumper.sln @@ -8,13 +8,19 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2087F99A-A655-41C1-84BB-54798AEA4080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2087F99A-A655-41C1-84BB-54798AEA4080}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2087F99A-A655-41C1-84BB-54798AEA4080}.Debug|x86.ActiveCfg = Debug|x86 + {2087F99A-A655-41C1-84BB-54798AEA4080}.Debug|x86.Build.0 = Debug|x86 {2087F99A-A655-41C1-84BB-54798AEA4080}.Release|Any CPU.ActiveCfg = Release|Any CPU {2087F99A-A655-41C1-84BB-54798AEA4080}.Release|Any CPU.Build.0 = Release|Any CPU + {2087F99A-A655-41C1-84BB-54798AEA4080}.Release|x86.ActiveCfg = Release|x86 + {2087F99A-A655-41C1-84BB-54798AEA4080}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Il2CppDumper/ExecutableFormats/NSO.cs b/Il2CppDumper/ExecutableFormats/NSO.cs index 4d964d8b..0aa58244 100644 --- a/Il2CppDumper/ExecutableFormats/NSO.cs +++ b/Il2CppDumper/ExecutableFormats/NSO.cs @@ -279,7 +279,7 @@ public NSO UnCompress() var unCompressedData = new byte[header.TextSegment.DecompressedSize]; using (var decoder = new Lz4DecoderStream(new MemoryStream(textBytes))) { - decoder.Read(unCompressedData, 0, unCompressedData.Length); + ReadAll(decoder, unCompressedData); } writer.Write(unCompressedData); } @@ -293,7 +293,7 @@ public NSO UnCompress() var unCompressedData = new byte[header.RoDataSegment.DecompressedSize]; using (var decoder = new Lz4DecoderStream(new MemoryStream(roDataBytes))) { - decoder.Read(unCompressedData, 0, unCompressedData.Length); + ReadAll(decoder, unCompressedData); } writer.Write(unCompressedData); } @@ -307,7 +307,7 @@ public NSO UnCompress() var unCompressedData = new byte[header.DataSegment.DecompressedSize]; using (var decoder = new Lz4DecoderStream(new MemoryStream(dataBytes))) { - decoder.Read(unCompressedData, 0, unCompressedData.Length); + ReadAll(decoder, unCompressedData); } writer.Write(unCompressedData); } @@ -322,6 +322,20 @@ public NSO UnCompress() return this; } + private static void ReadAll(Stream s, byte[] buffer) + { + int offset = 0; + while (offset < buffer.Length) + { + int read = s.Read(buffer, offset, buffer.Length - offset); + if (read == 0) + { + throw new EndOfStreamException("Unexpected end of stream while reading decompressed data."); + } + offset += read; + } + } + public override SectionHelper GetSectionHelper(int methodCount, int typeDefinitionsCount, int imageCount) { var sectionHelper = new SectionHelper(this, methodCount, typeDefinitionsCount, metadataUsagesCount, imageCount); diff --git a/Il2CppDumper/Il2CppDumper.csproj b/Il2CppDumper/Il2CppDumper.csproj index b015a913..c52af7f7 100644 --- a/Il2CppDumper/Il2CppDumper.csproj +++ b/Il2CppDumper/Il2CppDumper.csproj @@ -2,13 +2,14 @@ Exe - net6.0;net8.0 + net9.0-windows8.0 1.0.0.0 1.0.0.0 1.0.0.0 Copyright © Perfare 2016-2024 embedded true + AnyCPU;x86 diff --git a/Il2CppDumper/Program.cs b/Il2CppDumper/Program.cs index e09ab830..0c31acad 100644 --- a/Il2CppDumper/Program.cs +++ b/Il2CppDumper/Program.cs @@ -3,6 +3,9 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text.Json; +using System.Collections.Generic; +using System.Threading; +// Add WinForms reference for dialogs namespace Il2CppDumper { @@ -13,97 +16,82 @@ class Program [STAThread] static void Main(string[] args) { - config = JsonSerializer.Deserialize(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + @"config.json")); + // Ensure STAThread for WinForms dialogs + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + { + var thread = new Thread(() => Main(args)); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + return; + } + + config = JsonSerializer.Deserialize( + File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"))); + string il2cppPath = null; string metadataPath = null; string outputDir = null; - if (args.Length == 1) + // ─────────────── simple flag parser ─────────────── + var it = ((IEnumerable)args).GetEnumerator(); + while (it.MoveNext()) { - if (args[0] == "-h" || args[0] == "--help" || args[0] == "/?" || args[0] == "/h") + var arg = it.Current; + switch (arg) { - ShowHelp(); - return; + case "-h": case "--help": ShowHelp(); return; + case "-i": + case "--input": + if (!it.MoveNext()) { ShowHelp(); return; } + il2cppPath = it.Current; break; + case "-m": + case "--meta": + if (!it.MoveNext()) { ShowHelp(); return; } + metadataPath = it.Current; break; + case "-o": + case "--output": + if (!it.MoveNext()) { ShowHelp(); return; } + outputDir = it.Current; break; + default: + Console.WriteLine($"Unknown arg: {arg}"); ShowHelp(); return; } } - if (args.Length > 3) + // ───────────────────────────────────────────────── + + // GUI fall-backs + if (il2cppPath == null) { - ShowHelp(); - return; + var ofd = new OpenFileDialog { Filter = "Il2Cpp binary|*.*" }; + if (!ofd.ShowDialog()) return; + il2cppPath = ofd.FileName; } - if (args.Length > 1) + if (metadataPath == null) { - foreach (var arg in args) - { - if (File.Exists(arg)) - { - var file = File.ReadAllBytes(arg); - if (BitConverter.ToUInt32(file, 0) == 0xFAB11BAF) - { - metadataPath = arg; - } - else - { - il2cppPath = arg; - } - } - else if (Directory.Exists(arg)) - { - outputDir = Path.GetFullPath(arg) + Path.DirectorySeparatorChar; - } - } + var ofd = new OpenFileDialog { Filter = "global-metadata.dat|global-metadata.dat" }; + if (!ofd.ShowDialog()) return; + metadataPath = ofd.FileName; } - outputDir ??= AppDomain.CurrentDomain.BaseDirectory; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (outputDir == null) { - if (il2cppPath == null) - { - var ofd = new OpenFileDialog - { - Filter = "Il2Cpp binary file|*.*" - }; - if (ofd.ShowDialog()) - { - il2cppPath = ofd.FileName; - ofd.Filter = "global-metadata|global-metadata.dat"; - if (ofd.ShowDialog()) - { - metadataPath = ofd.FileName; - } - else - { - return; - } - } - else - { - return; - } - } + var fbd = new FolderBrowserDialog { Description = "Select output folder" }; + if (!fbd.ShowDialog()) return; + outputDir = fbd.SelectedPath + Path.DirectorySeparatorChar; } - if (il2cppPath == null) + + // ensure absolute + trailing slash + outputDir = Path.GetFullPath(outputDir) + Path.DirectorySeparatorChar; + + try { - ShowHelp(); - return; + if (Init(il2cppPath, metadataPath, out var metadata, out var il2Cpp)) + Dump(metadata, il2Cpp, outputDir); } - if (metadataPath == null) + catch (Exception ex) { - Console.WriteLine($"ERROR: Metadata file not found or encrypted."); - } - else - { - try - { - if (Init(il2cppPath, metadataPath, out var metadata, out var il2Cpp)) - { - Dump(metadata, il2Cpp, outputDir); - } - } - catch (Exception e) - { - Console.WriteLine(e); - } + Console.WriteLine(ex); } + if (config.RequireAnyKey) { Console.WriteLine("Press any key to exit..."); @@ -113,7 +101,22 @@ static void Main(string[] args) static void ShowHelp() { - Console.WriteLine($"usage: {AppDomain.CurrentDomain.FriendlyName} "); + Console.WriteLine("Il2CppDumper - Unity il2cpp reverse engineering tool\n"); + Console.WriteLine("Usage:"); + Console.WriteLine($" {AppDomain.CurrentDomain.FriendlyName} -i -m -o "); + Console.WriteLine(); + Console.WriteLine("Options:"); + Console.WriteLine(" -h, --help Show this help message and exit"); + Console.WriteLine(" -i, --input Path to il2cpp binary file"); + Console.WriteLine(" -m, --meta Path to global-metadata.dat file"); + Console.WriteLine(" -o, --output Output directory"); + Console.WriteLine(); + Console.WriteLine("If no arguments are provided, GUI dialogs will be shown."); + if (config.RequireAnyKey) + { + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(true); + } } private static bool Init(string il2cppPath, string metadataPath, out Metadata metadata, out Il2Cpp il2Cpp) diff --git a/Il2CppDumper/Resource1.Designer.cs b/Il2CppDumper/Resource1.Designer.cs index b887e61b..9f11276f 100644 --- a/Il2CppDumper/Resource1.Designer.cs +++ b/Il2CppDumper/Resource1.Designer.cs @@ -1,10 +1,10 @@ //------------------------------------------------------------------------------ // -// 此代码由工具生成。 -// 运行时版本:4.0.30319.42000 +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // -// 对此文件的更改可能会导致不正确的行为,并且如果 -// 重新生成代码,这些更改将会丢失。 +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. // //------------------------------------------------------------------------------ @@ -13,12 +13,12 @@ namespace Il2CppDumper { /// - /// 一个强类型的资源类,用于查找本地化的字符串等。 + /// A strongly-typed resource class, for looking up localized strings, etc. /// - // 此类是由 StronglyTypedResourceBuilder - // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 - // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen - // (以 /str 作为命令选项),或重新生成 VS 项目。 + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] @@ -33,7 +33,7 @@ internal Resource1() { } /// - /// 返回此类使用的缓存的 ResourceManager 实例。 + /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { @@ -47,8 +47,8 @@ internal Resource1() { } /// - /// 重写当前线程的 CurrentUICulture 属性,对 - /// 使用此强类型资源类的所有资源查找执行重写。 + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { @@ -61,7 +61,7 @@ internal Resource1() { } /// - /// 查找 System.Byte[] 类型的本地化资源。 + /// Looks up a localized resource of type System.Byte[]. /// internal static byte[] Il2CppDummyDll { get { diff --git a/Il2CppDumper/Utils/OpenFileDialog.cs b/Il2CppDumper/Utils/OpenFileDialog.cs index 80e959f2..3b8b9790 100644 --- a/Il2CppDumper/Utils/OpenFileDialog.cs +++ b/Il2CppDumper/Utils/OpenFileDialog.cs @@ -43,4 +43,33 @@ public bool ShowDialog() } } } + + public class FolderBrowserDialog + { + public string Description { get; set; } + public string SelectedPath { get; set; } + + public bool ShowDialog() + { + var dialog = (IFileDialog)(new FileOpenDialogRCW()); + dialog.GetOptions(out var options); + options |= FOS.FOS_PICKFOLDERS | FOS.FOS_FORCEFILESYSTEM | FOS.FOS_NOVALIDATE | FOS.FOS_DONTADDTORECENT; + dialog.SetOptions(options); + if (!string.IsNullOrEmpty(Description)) + { + dialog.SetTitle(Description); + } + if (dialog.Show(IntPtr.Zero) == 0) + { + dialog.GetResult(out var shellItem); + shellItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out var ppszName); + SelectedPath = ppszName; + return true; + } + else + { + return false; + } + } + } } \ No newline at end of file diff --git a/Il2CppDumper/config.json b/Il2CppDumper/config.json index 03184fc2..74e2c6c9 100644 --- a/Il2CppDumper/config.json +++ b/Il2CppDumper/config.json @@ -9,7 +9,7 @@ "GenerateDummyDll": true, "GenerateStruct": true, "DummyDllAddToken": true, - "RequireAnyKey": true, + "RequireAnyKey": false, "ForceIl2CppVersion": false, "ForceVersion": 16, "ForceDump": false, diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..7d7a1178 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,15 @@ +image: Visual Studio 2022 + +environment: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true' + +before_build: + - ps: dotnet --info + +build_script: + - cmd: powershell -NoProfile -ExecutionPolicy Bypass -File build-ci.ps1 -TargetFrameworks "net8.0" + - cmd: powershell -NoProfile -ExecutionPolicy Bypass -File build-ci.ps1 -TargetFrameworks "net9.0" -Rids "win-x64" + +artifacts: + - path: Il2CppDumper\\bin\\Release\\published\\**\\* + name: Published diff --git a/build-ci.ps1 b/build-ci.ps1 new file mode 100644 index 00000000..147646a9 --- /dev/null +++ b/build-ci.ps1 @@ -0,0 +1,115 @@ +Param( + [string[]]$TargetFrameworks = @('net8.0') +) + +Set-StrictMode -Version Latest +$project = Join-Path $PSScriptRoot 'Il2CppDumper\\Il2CppDumper.csproj' +$configuration = 'Release' +$outBase = Join-Path $PSScriptRoot 'Il2CppDumper\\bin\\Release' + +# Clean previous restore artifacts to avoid stale project.assets.json +$projectObj = Join-Path (Split-Path $project) 'obj' +if (Test-Path $projectObj) { + Write-Host "Removing stale obj folder: $projectObj" + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $projectObj +} + +$projectBin = Join-Path (Split-Path $project) 'bin' +if (Test-Path $projectBin) { + Write-Host "Removing stale bin folder: $projectBin" + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $projectBin +} + +function Run-RestoreForTfm { + param($tfm) + + Write-Host "Attempting dotnet restore for $tfm" + $restoreCmd = "dotnet restore `"$project`" --framework $tfm" + Write-Host "Running: $restoreCmd" + $restoreOutput = iex $restoreCmd 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Host "dotnet restore succeeded for $tfm" + return + } + Write-Host "dotnet restore failed for $tfm, falling back to msbuild restore. Output:"; + $restoreOutput | ForEach-Object { Write-Host $_ } + # Fallback + Write-Host "Restoring assets for $tfm using dotnet msbuild Restore" + $restoreCmd = "dotnet msbuild `"$project`" -t:Restore -p:TargetFramework=$tfm" + Write-Host "Running: $restoreCmd" + $restoreOutput = iex $restoreCmd 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "Restore command failed. Output:"; + $restoreOutput | ForEach-Object { Write-Host $_ } + throw "restore failed for $tfm" + } +} + +function Publish-Target { + param($tfm, $selfContained=$false) + + # Ensure per-TFM assets exist + Run-RestoreForTfm -tfm $tfm + + $selfArg = if ($selfContained) { ' --self-contained ' } else { ' --no-self-contained ' } + + $out = Join-Path $outBase "$tfm\publish" + Write-Host "Publishing $tfm -> $out" + # Framework-dependent publish (no single-file, no trimming) to avoid runtime pack/workload requirements on CI + $cmd = "dotnet publish `"$project`" -c $configuration -f $tfm -o `"$out`" $selfArg --no-restore" + Write-Host "Running: $cmd" + $output = iex $cmd 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "Publish command failed. Output:"; + $output | ForEach-Object { Write-Host $_ } + throw "publish failed for $tfm" + } + return $out +} + +function Safe-Copy { + param($src, $dst) + if (Test-Path $src) { + New-Item -ItemType Directory -Force -Path (Split-Path $dst) | Out-Null + Copy-Item $src -Destination $dst -Force + Write-Host "Copied $src -> $dst" + } else { + Write-Host "Skipping missing file $src" + } +} + +$artifacts = @() +$publishFailures = @() +foreach ($tfm in $TargetFrameworks) { + try { + $out = Publish-Target -tfm $tfm -selfContained:$false + $artifacts += $out + } catch { + $err = "Publish failed for $($tfm): $($_)" + Write-Host $err + $publishFailures += $err + } +} + +# If any mandatory publish failed, exit with non-zero to fail CI +if ($publishFailures.Count -gt 0) { + Write-Host "One or more publishes failed. Failing the CI." + $publishFailures | ForEach-Object { Write-Host $_ } + exit 1 +} + +# Copy or prepare artifact outputs +$finalDir = Join-Path $outBase 'published' +New-Item -ItemType Directory -Force -Path $finalDir | Out-Null +foreach ($a in $artifacts | Sort-Object -Unique) { + if (Test-Path $a) { + Get-ChildItem -Path $a -Filter 'Il2CppDumper*.exe' -File -Recurse | ForEach-Object { + Safe-Copy $_.FullName (Join-Path $finalDir $_.Name) + } + } +} + +Write-Host "Published artifacts to $finalDir" +Write-Host "##[set-output name=artifact-path]$finalDir" + +exit 0