├── IrdLibraryClient ├── GlobalUsings.cs ├── ApiConfig.cs ├── Compression │ ├── ICompressor.cs │ ├── GZipCompressor.cs │ ├── DeflateCompressor.cs │ ├── Compressor.cs │ ├── DecompressedContent.cs │ ├── CompressedContent.cs │ └── CompressionMessageHandler.cs ├── IIrdClient.cs ├── IrdClientFreeFr.cs ├── IrdClientAldos.cs ├── Utils │ ├── ConsoleLogger.cs │ ├── Utils.cs │ └── UriExtensions.cs ├── IrdLibraryClient.csproj ├── IrdFormat │ ├── Ird.cs │ ├── IrdParser.cs │ └── IsoHeaderParser.cs ├── Formatters │ └── NamingStyles.cs ├── Properties │ └── rd.xml ├── IrdClientFlexby420.cs ├── IrdClient.cs ├── Log.cs └── RedumpClient.cs ├── UI.Avalonia ├── Assets │ ├── icon.icns │ ├── icon.ico │ ├── Fonts │ │ └── Font Awesome 6 Free-Solid-900.otf │ └── icon.svg ├── Utils │ ├── DEV_BROADCAST_HDR.cs │ ├── DEV_BROADCAST_VOLUME.cs │ ├── DriveLetterToIdConverter.cs │ ├── OS.cs │ ├── WinRTInterop │ │ ├── Generated.cs │ │ ├── UIColorType.cs │ │ ├── CustomPlatformSettings.cs │ │ └── NativeWinRTMethods.cs │ ├── ColorPalette │ │ └── ThemeConsts.cs │ ├── Shobj.cs │ └── WndProcHelper.cs ├── Converters │ ├── SymbolConverterBase.cs │ ├── OnOffConverter.cs │ ├── ValidationSymbolConverter.cs │ ├── TextRenderingModeConverter.cs │ ├── BrushConverter.cs │ └── ColorConverter.cs ├── ViewModels │ ├── ErrorStubViewModel.cs │ ├── ErrorStubViewModel.windows.cs │ ├── MainViewModel.windows.cs │ ├── MainWindowViewModel.cs │ └── SettingsViewModel.cs ├── Views │ ├── Main.axaml.cs │ ├── Settings.axaml.cs │ ├── ErrorStub.axaml.cs │ ├── MainWindow.axaml.cs │ ├── MainWindow.axaml.windows.cs │ ├── ErrorStub.axaml │ ├── MainWindow.axaml.macos.cs │ └── MainWindow.axaml ├── Program.cs ├── App.axaml ├── app.manifest ├── ViewLocator.cs ├── UI.Avalonia.csproj └── App.axaml.cs ├── screenshots ├── fedora-dark.png ├── fedora-light.png ├── windows11-dark.png └── windows11-light.png ├── Ps3DiscDumper ├── Sfo │ ├── EntryFormat.cs │ ├── ParamSfoEntry.cs │ └── ParamSfo.cs ├── DiscInfo │ ├── FileInfo.cs │ ├── DiscInfo.cs │ └── DiscInfoConverter.cs ├── DiscKeyProviders │ ├── IDiscKeyProvider.cs │ ├── IrdProvider.cs │ └── RedumpProvider.cs ├── Utils │ ├── Patterns.cs │ ├── StreamEx.cs │ ├── SecurityEx.cs │ ├── StorageUnits.cs │ ├── HexExtensions.cs │ ├── PatternFormatter.cs │ ├── MacOS │ │ ├── IOKit.cs │ │ ├── DiskArbitration.cs │ │ └── CoreFoundation.cs │ └── IO.cs ├── Sfb │ ├── Sfb.cs │ └── SfbReader.cs ├── Ps3DiscDumper.csproj ├── SnakeCasePolicy.cs ├── POCOs │ └── GithubReleaseInfo.cs ├── DiscKeyInfo.cs ├── Settings.cs └── SettingsProvider.cs ├── Tests ├── BitFiddlingTests.cs ├── Tests.csproj └── IrdTests.cs ├── .vscode ├── tasks.json └── launch.json ├── Ps3DiscDumper.sln.DotSettings ├── LICENSE ├── publish-mac.ps1 ├── publish.ps1 ├── README.md ├── .gitattributes ├── Ps3DiscDumper.sln ├── .github └── workflows │ └── publish-release.yml └── .gitignore /IrdLibraryClient/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Text; 2 | -------------------------------------------------------------------------------- /UI.Avalonia/Assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/13xforever/ps3-disc-dumper/HEAD/UI.Avalonia/Assets/icon.icns -------------------------------------------------------------------------------- /UI.Avalonia/Assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/13xforever/ps3-disc-dumper/HEAD/UI.Avalonia/Assets/icon.ico -------------------------------------------------------------------------------- /screenshots/fedora-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/13xforever/ps3-disc-dumper/HEAD/screenshots/fedora-dark.png -------------------------------------------------------------------------------- /screenshots/fedora-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/13xforever/ps3-disc-dumper/HEAD/screenshots/fedora-light.png -------------------------------------------------------------------------------- /screenshots/windows11-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/13xforever/ps3-disc-dumper/HEAD/screenshots/windows11-dark.png -------------------------------------------------------------------------------- /screenshots/windows11-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/13xforever/ps3-disc-dumper/HEAD/screenshots/windows11-light.png -------------------------------------------------------------------------------- /IrdLibraryClient/ApiConfig.cs: -------------------------------------------------------------------------------- 1 | namespace IrdLibraryClient; 2 | 3 | public static class ApiConfig 4 | { 5 | public static readonly CancellationTokenSource Cts = new(); 6 | } -------------------------------------------------------------------------------- /UI.Avalonia/Assets/Fonts/Font Awesome 6 Free-Solid-900.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/13xforever/ps3-disc-dumper/HEAD/UI.Avalonia/Assets/Fonts/Font Awesome 6 Free-Solid-900.otf -------------------------------------------------------------------------------- /Ps3DiscDumper/Sfo/EntryFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Ps3DiscDumper.Sfo; 2 | 3 | public enum EntryFormat : ushort 4 | { 5 | Utf8 = 0x0004, 6 | Utf8Null = 0x0204, 7 | Int32 = 0x0404, 8 | } -------------------------------------------------------------------------------- /UI.Avalonia/Utils/DEV_BROADCAST_HDR.cs: -------------------------------------------------------------------------------- 1 | namespace UI.Avalonia.Utils; 2 | 3 | public struct DEV_BROADCAST_HDR 4 | { 5 | public int dbch_size; 6 | public int dbch_devicetype; 7 | public int dbch_reserved; 8 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/DiscInfo/FileInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ps3DiscDumper.DiscInfo; 4 | 5 | public class FileInfo 6 | { 7 | public long Offset; 8 | public long Size; 9 | public Dictionary> Hashes; 10 | } -------------------------------------------------------------------------------- /UI.Avalonia/Utils/DEV_BROADCAST_VOLUME.cs: -------------------------------------------------------------------------------- 1 | namespace UI.Avalonia.Utils; 2 | 3 | public struct DEV_BROADCAST_VOLUME 4 | { 5 | public int dbcv_size; 6 | public int dbcv_devicetype; 7 | public int dbcv_reserved; 8 | public int dbcv_unitmask; 9 | public int dbcv_flags; 10 | } -------------------------------------------------------------------------------- /IrdLibraryClient/Compression/ICompressor.cs: -------------------------------------------------------------------------------- 1 | namespace IrdLibraryClient.Compression; 2 | 3 | public interface ICompressor 4 | { 5 | string EncodingType { get; } 6 | Task CompressAsync(Stream source, Stream destination); 7 | Task DecompressAsync(Stream source, Stream destination); 8 | } -------------------------------------------------------------------------------- /Tests/BitFiddlingTests.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using NUnit.Framework; 3 | 4 | namespace Tests; 5 | 6 | [TestFixture] 7 | public class BitFiddlingTests 8 | { 9 | [Test] 10 | public void StringToInt() 11 | { 12 | Assert.That(BinaryPrimitives.ReadInt32BigEndian(".SFB"u8), Is.EqualTo(0x2e534642)); 13 | } 14 | } -------------------------------------------------------------------------------- /UI.Avalonia/Converters/SymbolConverterBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Avalonia.Media; 4 | 5 | namespace UI.Avalonia.Converters; 6 | 7 | public class SymbolConverterBase 8 | { 9 | protected static readonly Lazy HasFluentIcons = new(() => FontManager.Current.SystemFonts.Any(f => f.Name is "Segoe Fluent Icons")); 10 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/DiscKeyProviders/IDiscKeyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Ps3DiscDumper.DiscKeyProviders; 6 | 7 | public interface IDiscKeyProvider 8 | { 9 | Task> EnumerateAsync(string discKeyCachePath, string productCode, CancellationToken cancellationToken); 10 | } -------------------------------------------------------------------------------- /IrdLibraryClient/IIrdClient.cs: -------------------------------------------------------------------------------- 1 | using IrdLibraryClient.IrdFormat; 2 | 3 | namespace IrdLibraryClient; 4 | 5 | public interface IIrdClient 6 | { 7 | Uri BaseUri { get; } 8 | Task> GetFullListAsync(CancellationToken cancellationToken); 9 | Task DownloadAsync(string irdName, string localCachePath, CancellationToken cancellationToken); 10 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/Utils/Patterns.cs: -------------------------------------------------------------------------------- 1 | namespace Ps3DiscDumper.Utils; 2 | 3 | public static class Patterns 4 | { 5 | public const string ProductCode = "product_code"; 6 | public const string ProductCodeLetters = "product_code_letters"; 7 | public const string ProductCodeNumbers = "product_code_numbers"; 8 | public const string Title = "title"; 9 | public const string Region = "region"; 10 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/UI.Console/UI.Console.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /UI.Avalonia/Utils/DriveLetterToIdConverter.cs: -------------------------------------------------------------------------------- 1 | namespace UI.Avalonia.Utils; 2 | 3 | internal static class DriveLetterToIdConverter 4 | { 5 | public static int ToDriveId(this char driveLetter) 6 | { 7 | driveLetter = char.ToUpperInvariant(driveLetter); 8 | if (driveLetter < 'A' || driveLetter > 'Z') 9 | return 0; 10 | 11 | return 1 << (driveLetter - 'A'); 12 | } 13 | } -------------------------------------------------------------------------------- /UI.Avalonia/ViewModels/ErrorStubViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace UI.Avalonia.ViewModels; 4 | 5 | public partial class ErrorStubViewModel: ViewModelBase 6 | { 7 | [ObservableProperty] private bool uacIsEnabled = false; 8 | public string UacInfoLink => "https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/"; 9 | } -------------------------------------------------------------------------------- /Ps3DiscDumper.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True -------------------------------------------------------------------------------- /Ps3DiscDumper/DiscInfo/DiscInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ps3DiscDumper.DiscInfo; 4 | 5 | public class DiscInfo 6 | { 7 | public string ProductCode; // BLUS12345 8 | public string DiscVersion; // VERSION field from PARAM.SFO 9 | public string DiscKeyRawData; // IRD 10 | public string DiscKey; // Redump, and what is actually used for decryption 11 | public FileInfo DiscImage; 12 | public Dictionary Files; 13 | } -------------------------------------------------------------------------------- /UI.Avalonia/Converters/OnOffConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace UI.Avalonia.Converters; 6 | 7 | public class OnOffConverter: IValueConverter 8 | { 9 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 10 | => value is true ? "On" : "Off"; 11 | 12 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 13 | => throw new NotImplementedException(); 14 | } -------------------------------------------------------------------------------- /UI.Avalonia/Views/Main.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Interactivity; 3 | 4 | namespace UI.Avalonia.Views; 5 | 6 | public partial class Main : UserControl 7 | { 8 | public Main() => InitializeComponent(); 9 | 10 | private void OnLoaded(object? sender, RoutedEventArgs e) 11 | { 12 | } 13 | 14 | private void OnResized(object? sender, SizeChangedEventArgs e) 15 | { 16 | if (e.WidthChanged) 17 | ProgressBar.Width = 500 - ProgressBar.Margin.Right - e.NewSize.Width; 18 | } 19 | } -------------------------------------------------------------------------------- /IrdLibraryClient/IrdClientFreeFr.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace IrdLibraryClient; 4 | 5 | public partial class IrdClientFreeFr : IrdClient 6 | { 7 | public override Uri BaseUri { get; } = new("http://ps3ird.free.fr/"); 8 | protected override Regex IrdFilename { get; } = FreeFrLink(); 9 | 10 | [GeneratedRegex(@"href=""(?[A-F0-9_]+\.ird)""(.+?(?\w{4}\d{5}))", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Singleline)] 11 | private static partial Regex FreeFrLink(); 12 | } -------------------------------------------------------------------------------- /IrdLibraryClient/Compression/GZipCompressor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | 3 | 4 | namespace IrdLibraryClient.Compression; 5 | 6 | public class GZipCompressor : Compressor 7 | { 8 | public override string EncodingType => "gzip"; 9 | 10 | public override Stream CreateCompressionStream(Stream output) 11 | { 12 | return new GZipStream(output, CompressionMode.Compress, true); 13 | } 14 | 15 | public override Stream CreateDecompressionStream(Stream input) 16 | { 17 | return new GZipStream(input, CompressionMode.Decompress, true); 18 | } 19 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/Utils/StreamEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Ps3DiscDumper.Utils; 5 | 6 | public static class StreamEx 7 | { 8 | [Obsolete] 9 | public static int ReadExact(this Stream input, byte[] buffer, int offset, int count) 10 | { 11 | var result = 0; 12 | int read; 13 | do 14 | { 15 | read = input.Read(buffer, offset, count); 16 | result += read; 17 | offset += read; 18 | count -= read; 19 | } while (count > 0 && read > 0); 20 | return result; 21 | } 22 | } -------------------------------------------------------------------------------- /IrdLibraryClient/Compression/DeflateCompressor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | 3 | namespace IrdLibraryClient.Compression; 4 | 5 | public class DeflateCompressor : Compressor 6 | { 7 | public override string EncodingType => "deflate"; 8 | 9 | public override Stream CreateCompressionStream(Stream output) 10 | { 11 | return new DeflateStream(output, CompressionMode.Compress, true); 12 | } 13 | 14 | public override Stream CreateDecompressionStream(Stream input) 15 | { 16 | return new DeflateStream(input, CompressionMode.Decompress, true); 17 | } 18 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/Sfb/Sfb.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Ps3DiscDumper.Sfb; 5 | 6 | public class Sfb 7 | { 8 | public static readonly int Magic = BitConverter.ToInt32(".SFB"u8); 9 | public short VersionMajor; 10 | public short VersionMinor; 11 | public byte[] Unknown1; // 0x18 12 | public readonly List KeyEntries = []; 13 | } 14 | 15 | public class SfbKeyEntry 16 | { 17 | public string Key; // 0x10 18 | public int ValueOffset; 19 | public int ValueLength; 20 | public long Unknown; 21 | public string Value; 22 | } -------------------------------------------------------------------------------- /UI.Avalonia/Utils/OS.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UI.Avalonia.Utils; 4 | 5 | public static class OS 6 | { 7 | public static bool IsWin11 => OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000); 8 | public static bool IsWin10 => OperatingSystem.IsWindowsVersionAtLeast(10); 9 | public static bool IsWin81 => OperatingSystem.IsWindowsVersionAtLeast(8, 1); 10 | public static bool IsWin80 => OperatingSystem.IsWindowsVersionAtLeast(8); 11 | public static bool IsWin7 => OperatingSystem.IsWindowsVersionAtLeast(6, 1); 12 | public static bool IsWinVista => OperatingSystem.IsWindowsVersionAtLeast(6); 13 | } -------------------------------------------------------------------------------- /UI.Avalonia/Views/Settings.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Interactivity; 3 | using Avalonia.VisualTree; 4 | 5 | namespace UI.Avalonia.Views; 6 | 7 | public partial class Settings : UserControl 8 | { 9 | public Settings() => InitializeComponent(); 10 | 11 | private void OnLoad(object? sender, RoutedEventArgs e) 12 | { 13 | var m = SettingsPanel.Margin; 14 | var w = this.FindAncestorOfType()!; 15 | var additionalTop = w.IsExtendedIntoWindowDecorations ? 16 : 50; 16 | SettingsPanel.Margin = new(m.Left, m.Top + additionalTop, m.Right, m.Bottom + 16); 17 | } 18 | } -------------------------------------------------------------------------------- /IrdLibraryClient/IrdClientAldos.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace IrdLibraryClient; 4 | 5 | public partial class IrdClientAldos: IrdClient 6 | { 7 | public override Uri BaseUri { get; } = new Uri("http://ps3.aldostools.org/ird/").SetQueryParameters(new Dictionary 8 | { 9 | ["F"] = "0", 10 | ["_"] = DateTime.UtcNow.Ticks.ToString(), 11 | }); 12 | protected override Regex IrdFilename { get; } = AldosLink(); 13 | 14 | [GeneratedRegex(@"href=""(?(?\w{4}\d{5})-[A-F0-9]+\.ird)""", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Singleline)] 15 | private static partial Regex AldosLink(); 16 | } -------------------------------------------------------------------------------- /UI.Avalonia/Converters/ValidationSymbolConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace UI.Avalonia.Converters; 6 | 7 | public class ValidationSymbolConverter: SymbolConverterBase, IValueConverter 8 | { 9 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 10 | => value is bool b 11 | ? HasFluentIcons.Value 12 | ? b ? "\ue73e" : "\ueb90" 13 | : b ? "\uf058" : "\uf06a" 14 | : null; 15 | 16 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 17 | => throw new NotImplementedException(); 18 | } -------------------------------------------------------------------------------- /IrdLibraryClient/Utils/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | namespace IrdLibraryClient.Utils; 2 | 3 | public static class ConsoleLogger 4 | { 5 | public static void PrintError(Exception e, HttpResponseMessage? response, bool isError = true) 6 | { 7 | if (isError) 8 | Log.Error(e, "HTTP error"); 9 | else 10 | Log.Warn(e, "HTTP error"); 11 | if (response?.RequestMessage?.RequestUri is null) 12 | return; 13 | 14 | try 15 | { 16 | Log.Info(response.RequestMessage.RequestUri.ToString()); 17 | var msg = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); 18 | Log.Warn(msg); 19 | } 20 | catch { } 21 | } 22 | } -------------------------------------------------------------------------------- /UI.Avalonia/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | 4 | namespace UI.Avalonia; 5 | 6 | class Program 7 | { 8 | // Initialization code. Don't use any Avalonia, third-party APIs or any 9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 10 | // yet and stuff might break. 11 | [STAThread] 12 | public static void Main(string[] args) 13 | => BuildAvaloniaApp() 14 | .StartWithClassicDesktopLifetime(args); 15 | 16 | // Avalonia configuration, don't remove; also used by visual designer. 17 | public static AppBuilder BuildAvaloniaApp() 18 | => AppBuilder.Configure() 19 | .UsePlatformDetect() 20 | .WithInterFont() 21 | .LogToTrace(); 22 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/Ps3DiscDumper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | latest 6 | true 7 | 8 | 9 | 10 | TRIMMED 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Ps3DiscDumper/Utils/SecurityEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Principal; 4 | 5 | namespace Ps3DiscDumper.Utils; 6 | 7 | public static class SecurityEx 8 | { 9 | public static bool IsSafe(params string[] args) 10 | { 11 | if (OperatingSystem.IsWindows()) 12 | { 13 | using var identity = WindowsIdentity.GetCurrent(); 14 | var principal = new WindowsPrincipal(identity); 15 | if (principal.IsInRole(WindowsBuiltInRole.Administrator) 16 | && !args.Any(p => p.Equals("/IUnderstandThatRunningSoftwareAsAdministratorIsDangerousAndNotRecommendedForAnyone"))) 17 | { 18 | return false; 19 | } 20 | } 21 | return true; 22 | } 23 | } -------------------------------------------------------------------------------- /IrdLibraryClient/IrdLibraryClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Ps3DiscDumper/SnakeCasePolicy.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace Ps3DiscDumper; 5 | 6 | public class SnakeCasePolicy : JsonNamingPolicy 7 | { 8 | public override string ConvertName(string name) 9 | { 10 | var result = new StringBuilder(name.Length + 3); 11 | for (var i = 0; i < name.Length; i++) 12 | { 13 | var c = name[i]; 14 | if (char.IsLower(c)) 15 | result.Append(c); 16 | else 17 | { 18 | c = char.ToLower(c); 19 | if (i > 0) 20 | result.Append('_').Append(c); 21 | else 22 | result.Append(c); 23 | } 24 | } 25 | return result.ToString(); 26 | } 27 | } -------------------------------------------------------------------------------- /UI.Avalonia/ViewModels/ErrorStubViewModel.windows.cs: -------------------------------------------------------------------------------- 1 | #if WINDOWS 2 | using System; 3 | using System.Security.AccessControl; 4 | using IrdLibraryClient; 5 | using Microsoft.Win32; 6 | 7 | namespace UI.Avalonia.ViewModels; 8 | 9 | public partial class ErrorStubViewModel: ViewModelBase 10 | { 11 | 12 | public ErrorStubViewModel() 13 | { 14 | try 15 | { 16 | using var key = Registry.LocalMachine.OpenSubKey( 17 | @"Software\Microsoft\Windows\CurrentVersion\Policies\System", 18 | RegistryRights.ReadKey | RegistryRights.QueryValues 19 | ); 20 | UacIsEnabled = key?.GetValue("EnableLUA") is 1; 21 | } 22 | catch (Exception e) 23 | { 24 | Log.Warn(e, "Failed to check UAC status"); 25 | } 26 | } 27 | } 28 | #endif -------------------------------------------------------------------------------- /UI.Avalonia/Utils/WinRTInterop/Generated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using MicroCom.Runtime; 6 | 7 | namespace UI.Avalonia.Utils.WinRTInterop; 8 | 9 | internal enum TrustLevel 10 | { 11 | BaseTrust, 12 | PartialTrust, 13 | FullTrust 14 | } 15 | 16 | internal unsafe partial interface IInspectable : global::MicroCom.Runtime.IUnknown 17 | { 18 | void GetIids(ulong* iidCount, Guid** iids); 19 | IntPtr RuntimeClassName { get; } 20 | TrustLevel TrustLevel { get; } 21 | } 22 | 23 | internal unsafe partial interface IActivationFactory : IInspectable 24 | { 25 | IntPtr ActivateInstance(); 26 | } 27 | 28 | internal unsafe partial interface IUISettings3 : IInspectable 29 | { 30 | WinRTColor GetColorValue(UIColorType desiredColor); 31 | } 32 | -------------------------------------------------------------------------------- /Ps3DiscDumper/Utils/StorageUnits.cs: -------------------------------------------------------------------------------- 1 | namespace Ps3DiscDumper.Utils; 2 | 3 | public static class StorageUnits 4 | { 5 | private const long UnderKB = 1000L; 6 | private const long UnderMB = 1000L * 1024; 7 | private const long UnderGB = 1000L * 1024 * 1024; 8 | private const long UnderTB = 1000L * 1024 * 1024 * 1024; 9 | 10 | public static string AsStorageUnit(this long bytes) 11 | { 12 | if (bytes < UnderKB) 13 | return $"{bytes} byte{(bytes == 1 ? "" : "s")}"; 14 | if (bytes < UnderMB) 15 | return $"{bytes / 1024.0:0.##} KB"; 16 | if (bytes < UnderGB) 17 | return $"{bytes / 1024.0 / 1024:0.##} MB"; 18 | if (bytes < UnderTB) 19 | return $"{bytes / 1024.0 / 1024 / 1024:0.##} GB"; 20 | return $"{bytes / 1024.0 / 1024 / 1024:0.##} TB"; 21 | } 22 | } -------------------------------------------------------------------------------- /UI.Avalonia/Converters/TextRenderingModeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Controls; 4 | using Avalonia.Data.Converters; 5 | using Avalonia.Media; 6 | 7 | namespace UI.Avalonia.Converters; 8 | 9 | public class TextRenderingModeConverter: IValueConverter 10 | { 11 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 12 | => value is WindowTransparencyLevel wtl 13 | ? wtl == WindowTransparencyLevel.Mica || wtl == WindowTransparencyLevel.AcrylicBlur 14 | ? TextRenderingMode.Antialias 15 | : TextRenderingMode.SubpixelAntialias 16 | : null; 17 | 18 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 19 | => throw new NotImplementedException(); 20 | } -------------------------------------------------------------------------------- /UI.Avalonia/Utils/WinRTInterop/UIColorType.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using Avalonia.Media; 3 | 4 | namespace UI.Avalonia.Utils; 5 | 6 | public enum UIColorType 7 | { 8 | Background = 0, 9 | Foreground = 1, 10 | AccentDark3 = 2, 11 | AccentDark2 = 3, 12 | AccentDark1 = 4, 13 | Accent = 5, 14 | AccentLight1 = 6, 15 | AccentLight2 = 7, 16 | AccentLight3 = 8, 17 | Complement = 9 18 | } 19 | 20 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 21 | internal record struct WinRTColor 22 | { 23 | public byte A; 24 | public byte R; 25 | public byte G; 26 | public byte B; 27 | 28 | public static WinRTColor FromArgb(byte a, byte r, byte g, byte b) => new WinRTColor() 29 | { 30 | A = a, R = r, G = g, B = b 31 | }; 32 | 33 | public Color ToAvalonia() => new(A, R, G, B); 34 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/POCOs/GithubReleaseInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Ps3DiscDumper.POCOs; 5 | 6 | public class GitHubReleaseInfo 7 | { 8 | public string Url { get; set; } 9 | public string AssetsUrl { get; set; } 10 | public string HtmlUrl { get; set; } 11 | public int Id { get; set; } 12 | public string TagName { get; set; } 13 | public string Name { get; set; } 14 | public string Body { get; set; } 15 | public bool Prerelease { get; set; } 16 | public DateTime CreatedAt { get; set; } 17 | public DateTime PublishedAt { get; set; } 18 | } 19 | 20 | [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, WriteIndented = true)] 21 | [JsonSerializable(typeof(GitHubReleaseInfo[]))] 22 | internal partial class GithubReleaseSerializer: JsonSerializerContext; -------------------------------------------------------------------------------- /UI.Avalonia/Utils/ColorPalette/ThemeConsts.cs: -------------------------------------------------------------------------------- 1 | namespace UI.Avalonia.Utils.ColorPalette; 2 | 3 | public static class ThemeConsts 4 | { 5 | // ref https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Materials/Backdrop/MicaController.h#L34C61-L34C64 6 | public const string LightThemeTintColor = "#f3f3f3"; 7 | public const double LightThemeTintOpacity = 0.5; 8 | public const double LightThemeMaterialOpacity = 0.8; 9 | public const double LightThemeLuminosityOpacity = 0; 10 | 11 | public const string DarkThemeTintColor = "#202020"; 12 | public const double DarkThemeTintOpacity = 0.8; 13 | public const double DarkThemeMaterialOpacity = 0.8; 14 | public const double DarkThemeLuminosityOpacity = 0; 15 | 16 | public static readonly IPalette Debug = new DebugPalette(); 17 | public static readonly IPalette Dark = new FluentPaletteDark(); 18 | public static readonly IPalette Light = new FluentPaletteLight(); 19 | 20 | public const string BrandColor = "#0094ff"; 21 | } -------------------------------------------------------------------------------- /UI.Avalonia/Views/ErrorStub.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Controls; 4 | using Avalonia.Interactivity; 5 | using Avalonia.Markup.Xaml; 6 | using Avalonia.Media; 7 | using Avalonia.Platform; 8 | using UI.Avalonia.Utils.ColorPalette; 9 | 10 | namespace UI.Avalonia.Views; 11 | 12 | public partial class ErrorStub : Window 13 | { 14 | public ErrorStub() 15 | { 16 | InitializeComponent(); 17 | } 18 | 19 | public override void Show() 20 | { 21 | 22 | base.Show(); 23 | App.OnThemeChanged(this, EventArgs.Empty); 24 | App.OnPlatformColorsChanged(this, PlatformSettings?.GetColorValues() ?? new PlatformColorValues 25 | { 26 | AccentColor1 = Color.Parse(ThemeConsts.BrandColor), 27 | AccentColor2 = Color.Parse(ThemeConsts.BrandColor), 28 | AccentColor3 = Color.Parse(ThemeConsts.BrandColor), 29 | }); 30 | } 31 | 32 | private void Exit(object? sender, RoutedEventArgs e) => Close(); 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ilya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /UI.Avalonia/Converters/BrushConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Globalization; 4 | using Avalonia.Data.Converters; 5 | using Avalonia.Media; 6 | 7 | namespace UI.Avalonia.Converters; 8 | 9 | public class BrushConverter: IValueConverter 10 | { 11 | private static readonly ConcurrentDictionary KnownBrushes = new(); 12 | 13 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 14 | { 15 | if (value is string { Length: > 0 } s) 16 | return Parse(s); 17 | return default(Brush); 18 | } 19 | 20 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 21 | => throw new NotImplementedException(); 22 | 23 | internal static IBrush Parse(string color) 24 | { 25 | if (KnownBrushes.TryGetValue(color, out var result)) 26 | return result; 27 | 28 | var c = ColorConverter.Parse(color); 29 | result = new SolidColorBrush(c, c.A / 255.0); 30 | KnownBrushes.TryAdd(color, result); 31 | return result; 32 | } 33 | } -------------------------------------------------------------------------------- /UI.Avalonia/Converters/ColorConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Globalization; 4 | using Avalonia.Data.Converters; 5 | using Avalonia.Media; 6 | 7 | namespace UI.Avalonia.Converters; 8 | 9 | public class ColorConverter: IValueConverter 10 | { 11 | private static readonly ConcurrentDictionary KnownColors = new(); 12 | 13 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 14 | { 15 | if (value is string { Length: > 0 } s) 16 | return Parse(s); 17 | return default(Color); 18 | } 19 | 20 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 21 | { 22 | if (value is Color c) 23 | return c.ToString(); 24 | return default(Color).ToString(); 25 | } 26 | 27 | internal static Color Parse(string color) 28 | { 29 | if (KnownColors.TryGetValue(color, out var result)) 30 | return result; 31 | 32 | result = Color.Parse(color); 33 | KnownColors.TryAdd(color, result); 34 | return result; 35 | } 36 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/Utils/HexExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text; 4 | 5 | namespace Ps3DiscDumper.Utils; 6 | 7 | public static class HexExtensions 8 | { 9 | public static byte[] ToByteArray(this string hexString) 10 | { 11 | if (hexString == null) 12 | return null; 13 | 14 | if (hexString.Length == 0) 15 | return []; 16 | 17 | if (hexString.Length % 2 != 0) 18 | throw new FormatException("Not a valid hex string"); 19 | 20 | var result = new byte[hexString.Length / 2]; 21 | for (var i = 0; i < hexString.Length; i += 2) 22 | result[i / 2] = byte.Parse(hexString.Substring(i, 2), NumberStyles.HexNumber); 23 | return result; 24 | } 25 | 26 | public static string ToHexString(this byte[] bytes) 27 | { 28 | if (bytes == null) 29 | return null; 30 | 31 | if (bytes.Length == 0) 32 | return ""; 33 | 34 | var result = new StringBuilder(bytes.Length*2); 35 | foreach (var b in bytes) 36 | result.Append(b.ToString("x2")); 37 | return result.ToString(); 38 | } 39 | } -------------------------------------------------------------------------------- /IrdLibraryClient/IrdFormat/Ird.cs: -------------------------------------------------------------------------------- 1 | namespace IrdLibraryClient.IrdFormat; 2 | 3 | public class Ird 4 | { 5 | public static readonly int Magic = BitConverter.ToInt32(Encoding.ASCII.GetBytes("3IRD"), 0); 6 | public byte Version; 7 | public string ProductCode = null!; // 9 8 | public byte TitleLength; 9 | public string Title = null!; 10 | public string? UpdateVersion; // 4 11 | public string? GameVersion; // 5 12 | public string? AppVersion; // 5 13 | public int Id; // v7 only? 14 | public int HeaderLength; 15 | public byte[] Header = null!; // gz 16 | public int FooterLength; 17 | public byte[] Footer = null!; // gz 18 | public byte RegionCount; 19 | public List RegionMd5Checksums = null!; // 16 each 20 | public int FileCount; 21 | public List Files = []; 22 | public int Unknown; // always 0? 23 | public byte[]? Pic; // 115, v9 only? 24 | public byte[] Data1 = null!; // 16 25 | public byte[] Data2 = null!; // 16 26 | // Pic for 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | avares://ps3-disc-dumper/Assets/Fonts#Font Awesome 6 Free 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /UI.Avalonia/Utils/WinRTInterop/CustomPlatformSettings.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media; 2 | using UI.Avalonia.Utils.WinRTInterop; 3 | 4 | namespace UI.Avalonia.Utils; 5 | 6 | public record AccentColorInfo(Color Accent, Color Light1, Color Light2, Color Light3, Color Dark1, Color Dark2, Color Dark3); 7 | 8 | public class CustomPlatformSettings 9 | { 10 | public static AccentColorInfo GetColorValues() 11 | { 12 | var uiSettings = NativeWinRTMethods.CreateInstance("Windows.UI.ViewManagement.UISettings"); 13 | var accent = uiSettings.GetColorValue(UIColorType.Accent).ToAvalonia(); 14 | var light1 = uiSettings.GetColorValue(UIColorType.AccentLight1).ToAvalonia(); 15 | var light2 = uiSettings.GetColorValue(UIColorType.AccentLight2).ToAvalonia(); 16 | var light3 = uiSettings.GetColorValue(UIColorType.AccentLight3).ToAvalonia(); 17 | var dark1 = uiSettings.GetColorValue(UIColorType.AccentDark1).ToAvalonia(); 18 | var dark2 = uiSettings.GetColorValue(UIColorType.AccentDark2).ToAvalonia(); 19 | var dark3 = uiSettings.GetColorValue(UIColorType.AccentDark3).ToAvalonia(); 20 | return new (accent, light1, light2, light3, dark1, dark2, dark3); 21 | } 22 | } -------------------------------------------------------------------------------- /UI.Avalonia/Utils/Shobj.cs: -------------------------------------------------------------------------------- 1 | #if WINDOWS 2 | using System; 3 | using TerraFX.Interop.Windows; 4 | using System.Runtime.InteropServices; 5 | using static TerraFX.Interop.Windows.Windows; 6 | 7 | namespace UI.Avalonia.Utils; 8 | 9 | internal sealed unsafe class Shobj: IDisposable 10 | { 11 | private ComPtr taskbar; 12 | 13 | public ITaskbarList3* Taskbar => taskbar.Get(); 14 | 15 | public Shobj() 16 | { 17 | fixed (ITaskbarList3** ptr = taskbar) 18 | { 19 | var hr = CoCreateInstance( 20 | __uuidof(), 21 | null, 22 | (uint)CLSCTX.CLSCTX_INPROC_SERVER, 23 | __uuidof(), 24 | (void**)ptr 25 | ); 26 | if (hr.FAILED) 27 | Marshal.ThrowExceptionForHR(hr.Value); 28 | } 29 | } 30 | 31 | ~Shobj() 32 | { 33 | Dispose(false); 34 | } 35 | 36 | private void Dispose(bool disposing) 37 | { 38 | if (disposing) 39 | taskbar.Dispose(); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | Dispose(true); 45 | GC.SuppressFinalize(this); 46 | } 47 | } 48 | #endif -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/UI.Console/bin/Debug/netcoreapp2.1/ps3-disc-dumper.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/UI.Console", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ,] 28 | } -------------------------------------------------------------------------------- /IrdLibraryClient/Formatters/NamingStyles.cs: -------------------------------------------------------------------------------- 1 | namespace IrdLibraryClient; 2 | 3 | public static class NamingStyles 4 | { 5 | public static string CamelCase(string value) 6 | { 7 | if (value == null) 8 | throw new ArgumentNullException(nameof(value)); 9 | 10 | if (value.Length > 0) 11 | { 12 | if (char.IsUpper(value[0])) 13 | value = char.ToLower(value[0]) + value[1..]; 14 | } 15 | return value; 16 | } 17 | 18 | public static string Dashed(string value) => Delimited(value, '-'); 19 | public static string Underscore(string value) => Delimited(value, '_'); 20 | 21 | private static string Delimited(string value, char separator) 22 | { 23 | if (value.Length == 0) 24 | return value; 25 | 26 | var hasPrefix = true; 27 | var builder = new StringBuilder(value.Length + 3); 28 | foreach (var c in value) 29 | { 30 | var ch = c; 31 | if (char.IsUpper(ch)) 32 | { 33 | ch = char.ToLower(ch); 34 | if (!hasPrefix) 35 | builder.Append(separator); 36 | hasPrefix = true; 37 | } 38 | else 39 | hasPrefix = false; 40 | builder.Append(ch); 41 | } 42 | return builder.ToString(); 43 | } 44 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/Utils/PatternFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace Ps3DiscDumper.Utils; 7 | 8 | public static class PatternFormatter 9 | { 10 | private static readonly char[] InvalidChars = [ 11 | ..((char[])[ 12 | ..Path.GetInvalidFileNameChars(), 13 | ..Path.GetInvalidPathChars(), 14 | ':', '/', '\\', '?', '*', '<', '>', '|' 15 | ]).Distinct() 16 | ]; 17 | 18 | public static string Format(string pattern, NameValueCollection items) 19 | { 20 | if (string.IsNullOrEmpty(pattern)) 21 | return pattern; 22 | 23 | foreach (var k in items.AllKeys) 24 | pattern = pattern.Replace($"%{k}%", items[k]); 25 | pattern = pattern.Replace("®", "") 26 | .Replace("™", "") 27 | .Replace("(TM)", "") 28 | .Replace("(R)", ""); 29 | pattern = ReplaceInvalidChars(pattern); 30 | if (string.IsNullOrEmpty(pattern)) 31 | pattern = ReplaceInvalidChars($"{DateTime.Now:s}"); 32 | return pattern; 33 | } 34 | 35 | private static string ReplaceInvalidChars(string dirName) 36 | { 37 | foreach (var invalidChar in InvalidChars) 38 | dirName = dirName.Replace(invalidChar, '_'); 39 | return dirName; 40 | } 41 | } -------------------------------------------------------------------------------- /IrdLibraryClient/Compression/Compressor.cs: -------------------------------------------------------------------------------- 1 | namespace IrdLibraryClient.Compression; 2 | 3 | public abstract class Compressor : ICompressor 4 | { 5 | public abstract string EncodingType { get; } 6 | public abstract Stream CreateCompressionStream(Stream output); 7 | public abstract Stream CreateDecompressionStream(Stream input); 8 | 9 | public virtual async Task CompressAsync(Stream source, Stream destination) 10 | { 11 | await using (var memStream = new MemoryStream()) 12 | { 13 | await using (var compressed = CreateCompressionStream(memStream)) 14 | await source.CopyToAsync(compressed).ConfigureAwait(false); 15 | memStream.Seek(0, SeekOrigin.Begin); 16 | await memStream.CopyToAsync(destination).ConfigureAwait(false); 17 | return memStream.Length; 18 | } 19 | } 20 | 21 | public virtual async Task DecompressAsync(Stream source, Stream destination) 22 | { 23 | await using (var memStream = new MemoryStream()) 24 | { 25 | await using (var decompressed = CreateDecompressionStream(source)) 26 | await decompressed.CopyToAsync(memStream).ConfigureAwait(false); 27 | memStream.Seek(0, SeekOrigin.Begin); 28 | await memStream.CopyToAsync(destination).ConfigureAwait(false); 29 | return memStream.Length; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /UI.Avalonia/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /IrdLibraryClient/Utils/Utils.cs: -------------------------------------------------------------------------------- 1 | namespace IrdLibraryClient.Utils; 2 | 3 | public static class Utils 4 | { 5 | public static string Trim(this string str, int maxLength) 6 | { 7 | const int minSaneLimit = 4; 8 | 9 | if (maxLength < minSaneLimit) 10 | throw new ArgumentException("Argument cannot be less than " + minSaneLimit, nameof(maxLength)); 11 | 12 | if (string.IsNullOrEmpty(str)) 13 | return str; 14 | 15 | if (str.Length > maxLength) 16 | return str[..(maxLength - 3)] + "..."; 17 | 18 | return str; 19 | } 20 | 21 | public static string Truncate(this string str, int maxLength) 22 | { 23 | if (maxLength < 1) 24 | throw new ArgumentException("Argument must be positive, but was " + maxLength, nameof(maxLength)); 25 | 26 | if (string.IsNullOrEmpty(str) || str.Length <= maxLength) 27 | return str; 28 | 29 | return str[..maxLength]; 30 | } 31 | 32 | public static string? Sanitize(this string? str) 33 | { 34 | return str?.Replace("`", "`\u200d").Replace("@", "@\u200d"); 35 | } 36 | 37 | public static int Clamp(this int amount, int low, int high) 38 | { 39 | return Math.Min(high, Math.Max(amount, low)); 40 | } 41 | 42 | public static string FixDiscFsPath(this string path) 43 | => path.Replace('\\', Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar); 44 | } -------------------------------------------------------------------------------- /publish-mac.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/pwsh 2 | Clear-Host 3 | 4 | if ($PSVersionTable.PSVersion.Major -lt 6) 5 | { 6 | Write-Host 'Restarting using pwsh...' 7 | pwsh $PSCommandPath 8 | return 9 | } 10 | 11 | Write-Host 'Clearing bin/obj...' -ForegroundColor Cyan 12 | Remove-Item -LiteralPath UI.Avalonia/bin -Recurse -Force -ErrorAction SilentlyContinue 13 | Remove-Item -LiteralPath UI.Avalonia/obj -Recurse -Force -ErrorAction SilentlyContinue 14 | 15 | Write-Host 'Building macOS binary...' -ForegroundColor Cyan 16 | dotnet publish -v:q -t:BundleApp -r osx-arm64 -f net10.0 --self-contained -c MacOS -o distrib/gui/mac/ UI.Avalonia/UI.Avalonia.csproj /p:PublishTrimmed=False /p:PublishSingleFile=True 17 | 18 | Write-Host 'Clearing extra files in distrib...' -ForegroundColor Cyan 19 | Get-ChildItem -LiteralPath distrib -Include *.pdb,*.config -Recurse | Remove-Item 20 | 21 | if (($LASTEXITCODE -eq 0) -and ($IsMacOS -or ($PSVersionTable.Platform -eq 'Unix'))) 22 | { 23 | chmod +x distrib/gui/mac/ps3-disc-dumper 24 | # The final app bundle needs to be re-signed as a whole. 25 | codesign --deep -fs - 'distrib/gui/mac/PS3 Disc Dumper.app' 26 | } 27 | 28 | Write-Host 'Bundling...' -ForegroundColor Cyan 29 | if (Test-Path -LiteralPath 'distrib/gui/mac/PS3 Disc Dumper.app') 30 | { 31 | # tar to preserve file permissions. 32 | tar -C distrib/gui/mac -cvzf distrib/ps3-disc-dumper_macos_NEW.tar.gz 'PS3 Disc Dumper.app' 33 | } 34 | 35 | Write-Host 'Done' -ForegroundColor Cyan 36 | 37 | 38 | -------------------------------------------------------------------------------- /UI.Avalonia/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Avalonia.Controls; 4 | using Avalonia.Controls.Templates; 5 | using UI.Avalonia.ViewModels; 6 | using UI.Avalonia.Views; 7 | 8 | namespace UI.Avalonia; 9 | 10 | public class ViewLocator : IDataTemplate 11 | { 12 | public Control Build(object? data) 13 | { 14 | var vmType = data?.GetType(); 15 | if (vmType == typeof(MainWindowViewModel)) 16 | return new MainWindow(); 17 | if (vmType == typeof(MainViewModel)) 18 | return new Main(); 19 | if (vmType == typeof(SettingsViewModel)) 20 | return new Settings(); 21 | if (vmType == typeof(ErrorStubViewModel)) 22 | return new ErrorStub(); 23 | 24 | var vmName = vmType?.FullName!; 25 | /* 26 | if (!vmName.EndsWith("ViewModel")) 27 | return new TextBlock { Text = "Not a ViewModel: " + vmName }; 28 | 29 | var name = vmName.Replace("ViewModel", "View"); 30 | if (name is {Length: >0} && Type.GetType(name) is Type viewType) 31 | return (Control)Activator.CreateInstance(viewType)!; 32 | 33 | name = name[..^4]; 34 | if (name is {Length: >0} && Type.GetType(name) is Type type) 35 | return (Control)Activator.CreateInstance(type)!; 36 | */ 37 | 38 | return new TextBlock { Text = "Couldn't fine view for " + vmName }; 39 | } 40 | 41 | public bool Match(object? data) => data is ViewModelBase; 42 | } -------------------------------------------------------------------------------- /IrdLibraryClient/Compression/DecompressedContent.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace IrdLibraryClient.Compression; 4 | 5 | public class DecompressedContent : HttpContent 6 | { 7 | private readonly HttpContent content; 8 | private readonly ICompressor compressor; 9 | 10 | public DecompressedContent(HttpContent content, ICompressor compressor) 11 | { 12 | if (content == null) 13 | throw new ArgumentNullException(nameof(content)); 14 | 15 | if (compressor == null) 16 | throw new ArgumentNullException(nameof(compressor)); 17 | 18 | this.content = content; 19 | this.compressor = compressor; 20 | RemoveHeaders(); 21 | } 22 | 23 | protected override bool TryComputeLength(out long length) 24 | { 25 | length = -1; 26 | return false; 27 | } 28 | 29 | protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) 30 | { 31 | if (stream == null) 32 | throw new ArgumentNullException(nameof(stream)); 33 | 34 | using (content) 35 | { 36 | var contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false); 37 | var decompressedLength = await compressor.DecompressAsync(contentStream, stream).ConfigureAwait(false); 38 | Headers.ContentLength = decompressedLength; 39 | } 40 | } 41 | 42 | private void RemoveHeaders() 43 | { 44 | foreach (var header in content.Headers) 45 | Headers.TryAddWithoutValidation(header.Key, header.Value); 46 | Headers.ContentEncoding.Clear(); 47 | Headers.ContentLength = null; 48 | } 49 | } -------------------------------------------------------------------------------- /IrdLibraryClient/Compression/CompressedContent.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace IrdLibraryClient.Compression; 4 | 5 | public class CompressedContent : HttpContent 6 | { 7 | private readonly HttpContent content; 8 | private readonly ICompressor compressor; 9 | 10 | public CompressedContent(HttpContent content, ICompressor compressor) 11 | { 12 | if (content == null) 13 | throw new ArgumentNullException(nameof(content)); 14 | 15 | if (compressor == null) 16 | throw new ArgumentNullException(nameof(compressor)); 17 | 18 | this.content = content; 19 | this.compressor = compressor; 20 | AddHeaders(); 21 | } 22 | 23 | protected override bool TryComputeLength(out long length) 24 | { 25 | length = -1; 26 | return false; 27 | } 28 | 29 | protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) 30 | { 31 | if (stream == null) 32 | throw new ArgumentNullException(nameof(stream)); 33 | 34 | using (content) 35 | { 36 | var contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false); 37 | var compressedLength = await compressor.CompressAsync(contentStream, stream).ConfigureAwait(false); 38 | Headers.ContentLength = compressedLength; 39 | } 40 | } 41 | 42 | private void AddHeaders() 43 | { 44 | foreach (var header in content.Headers) 45 | Headers.TryAddWithoutValidation(header.Key, header.Value); 46 | Headers.ContentEncoding.Add(compressor.EncodingType); 47 | Headers.ContentLength = null; 48 | } 49 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/Utils/MacOS/IOKit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using System.Runtime.Versioning; 5 | 6 | namespace Ps3DiscDumper.Utils.MacOS; 7 | 8 | [SupportedOSPlatform("osx")] 9 | public static partial class IOKit 10 | { 11 | private const string LibraryName = "/System/Library/Frameworks/IOKit.framework/Versions/Current/IOKit"; 12 | 13 | public static readonly IntPtr MasterPortDefault = IntPtr.Zero; 14 | public const string BdMediaClass = "IOBDMedia"; 15 | public static readonly IntPtr BdMediaClassCfString = CoreFoundation.__CFStringMakeConstantString(BdMediaClass); 16 | public static readonly IntPtr BsdNameKey = CoreFoundation.__CFStringMakeConstantString("BSD Name"); 17 | 18 | [LibraryImport(LibraryName, StringMarshalling = StringMarshalling.Utf8)] 19 | [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 20 | public static partial IntPtr IOServiceMatching(string name); 21 | 22 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 23 | public static partial uint IOServiceGetMatchingServices(IntPtr mainPort, IntPtr matching, out IntPtr existing); 24 | 25 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 26 | public static partial IntPtr IOIteratorNext(IntPtr iterator); 27 | 28 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 29 | public static partial IntPtr IORegistryEntryCreateCFProperty(IntPtr entry, IntPtr key, IntPtr allocator, uint options); 30 | 31 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 32 | public static partial IntPtr IOObjectRelease(IntPtr obj); 33 | } 34 | -------------------------------------------------------------------------------- /IrdLibraryClient/Properties/rd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /publish.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/pwsh 2 | Clear-Host 3 | $trim = $true 4 | 5 | if ($PSVersionTable.PSVersion.Major -lt 6) 6 | { 7 | Write-Host 'Restarting using pwsh...' 8 | pwsh $PSCommandPath 9 | return 10 | } 11 | 12 | Write-Host 'Clearing bin/obj...' -ForegroundColor Cyan 13 | Remove-Item -LiteralPath UI.Avalonia/bin -Recurse -Force -ErrorAction SilentlyContinue 14 | Remove-Item -LiteralPath UI.Avalonia/obj -Recurse -Force -ErrorAction SilentlyContinue 15 | 16 | if ($IsWindows -or ($PSVersionTable.Platform -eq 'Win32NT')) 17 | { 18 | Write-Host 'Building Windows binaries...' -ForegroundColor Cyan 19 | dotnet publish -v:q -r win-x64 -f net10.0-windows --self-contained -c Release -o distrib/gui/win/ UI.Avalonia/UI.Avalonia.csproj /p:PublishTrimmed=$trim /p:PublishSingleFile=True 20 | } 21 | 22 | Write-Host 'Building Linux binary...' -ForegroundColor Cyan 23 | dotnet publish -v:q -r linux-x64 -f net10.0 --self-contained -c Linux -o distrib/gui/lin/ UI.Avalonia/UI.Avalonia.csproj /p:PublishTrimmed=$trim /p:PublishSingleFile=True 24 | if (($LASTEXITCODE -eq 0) -and ($IsLinux -or ($PSVersionTable.Platform -eq 'Unix'))) 25 | { 26 | chmod +x distrib/gui/lin/ps3-disc-dumper 27 | } 28 | 29 | Write-Host 'Clearing extra files in distrib...' -ForegroundColor Cyan 30 | Get-ChildItem -LiteralPath distrib -Include *.pdb,*.config -Recurse | Remove-Item 31 | 32 | Write-Host 'Zipping...' -ForegroundColor Cyan 33 | if (Test-Path -LiteralPath distrib/gui/win/ps3-disc-dumper.exe) 34 | { 35 | Compress-Archive -LiteralPath distrib/gui/win/ps3-disc-dumper.exe -DestinationPath distrib/ps3-disc-dumper_windows_NEW.zip -CompressionLevel Optimal -Force 36 | } 37 | if (Test-Path -LiteralPath distrib/gui/lin/ps3-disc-dumper) 38 | { 39 | Compress-Archive -LiteralPath distrib/gui/lin/ps3-disc-dumper -DestinationPath distrib/ps3-disc-dumper_linux_NEW.zip -CompressionLevel Optimal -Force 40 | } 41 | 42 | Write-Host 'Done' -ForegroundColor Cyan 43 | 44 | 45 | -------------------------------------------------------------------------------- /UI.Avalonia/ViewModels/MainViewModel.windows.cs: -------------------------------------------------------------------------------- 1 | #if WINDOWS 2 | using System; 3 | using Avalonia; 4 | using Avalonia.Controls; 5 | using Avalonia.Controls.ApplicationLifetimes; 6 | using TerraFX.Interop.Windows; 7 | using UI.Avalonia.Utils; 8 | using ITaskbarList3 = TerraFX.Interop.Windows.ITaskbarList3; 9 | 10 | namespace UI.Avalonia.ViewModels; 11 | 12 | public unsafe partial class MainViewModel 13 | { 14 | private static readonly Shobj shobj = new(); 15 | 16 | private bool TryGetMainHandle(out HWND hwnd) 17 | { 18 | hwnd = default; 19 | if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime { MainWindow: Window w } 20 | || w.TryGetPlatformHandle() is not { } platformHandle) 21 | return false; 22 | 23 | hwnd = (HWND)platformHandle.Handle; 24 | return true; 25 | } 26 | 27 | partial void ResetTaskbarProgress() 28 | { 29 | if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1) 30 | || !TryGetMainHandle(out var hwnd)) 31 | return; 32 | 33 | try 34 | { 35 | shobj.Taskbar->SetProgressState(hwnd, TBPFLAG.TBPF_NOPROGRESS); 36 | shobj.Taskbar->SetProgressValue(hwnd, 0ul, (ulong)ProgressMax); 37 | } 38 | catch (InvalidOperationException) 39 | { 40 | //ignore in design mode 41 | } 42 | } 43 | 44 | partial void EnableTaskbarProgress() 45 | { 46 | if (OperatingSystem.IsWindowsVersionAtLeast(6, 1) 47 | && TryGetMainHandle(out var hwnd)) 48 | shobj.Taskbar->SetProgressState(hwnd, TBPFLAG.TBPF_NORMAL); 49 | } 50 | 51 | partial void SetTaskbarProgress(int position) 52 | { 53 | if (OperatingSystem.IsWindowsVersionAtLeast(6, 1) 54 | && TryGetMainHandle(out var hwnd)) 55 | shobj.Taskbar->SetProgressValue(hwnd, (ulong)position, (ulong)ProgressMax); 56 | } 57 | } 58 | #endif -------------------------------------------------------------------------------- /UI.Avalonia/Utils/WndProcHelper.cs: -------------------------------------------------------------------------------- 1 | #if WINDOWS 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using System.Runtime.Versioning; 5 | using Avalonia.Controls; 6 | using IrdLibraryClient; 7 | using TerraFX.Interop.Windows; 8 | 9 | namespace UI.Avalonia.Utils; 10 | 11 | [SupportedOSPlatform("windows")] 12 | public static unsafe class WndProcHelper 13 | { 14 | public delegate void OnWndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam); 15 | 16 | private static delegate* unmanaged wrappedWndProc; 17 | private static OnWndProc userFunc = null!; 18 | 19 | public static bool Register(Window window, OnWndProc onWndProc) 20 | { 21 | try 22 | { 23 | userFunc = onWndProc; 24 | if (window.TryGetPlatformHandle() is not { } platformHandle) 25 | return false; 26 | 27 | var hwnd = (HWND)platformHandle.Handle; 28 | wrappedWndProc = (delegate* unmanaged)TerraFX.Interop.Windows.Windows.GetWindowLongPtr(hwnd, GWLP.GWLP_WNDPROC); 29 | delegate* unmanaged wrapperWinProc = &WndProcHook; 30 | TerraFX.Interop.Windows.Windows.SetWindowLongPtr(hwnd, GWLP.GWLP_WNDPROC, (nint)wrapperWinProc); 31 | return true; 32 | } 33 | catch (Exception e) 34 | { 35 | Log.Error(e, "Failed to hook the WndProc event loop"); 36 | return false; 37 | } 38 | } 39 | 40 | [UnmanagedCallersOnly] 41 | public static LRESULT WndProcHook(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam) 42 | { 43 | try 44 | { 45 | userFunc(hwnd, msg, wParam, lParam); 46 | } 47 | catch (Exception e) 48 | { 49 | Log.Error(e, "Exception in user-defined event loop hook"); 50 | } 51 | return wrappedWndProc(hwnd, msg, wParam, lParam); 52 | } 53 | } 54 | #endif -------------------------------------------------------------------------------- /Ps3DiscDumper/Sfb/SfbReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace Ps3DiscDumper.Sfb; 7 | 8 | public static class SfbReader 9 | { 10 | public static Sfb Parse(byte[] content) 11 | { 12 | if (content == null) 13 | throw new ArgumentNullException(nameof(content)); 14 | 15 | if (content.Length < 200) 16 | throw new ArgumentException("Data is too small to be a valid SFB structure", nameof(content)); 17 | 18 | if (BitConverter.ToInt32(content.AsSpan(0, 4)) != Sfb.Magic) 19 | throw new ArgumentException("Specified file is not a valid SFB file", nameof(content)); 20 | 21 | var result = new Sfb(); 22 | using var stream = new MemoryStream(content, false); 23 | using var reader = new BinaryReader(stream, Encoding.ASCII); 24 | reader.ReadInt32(); // magic 25 | result.VersionMajor = BinaryPrimitives.ReadInt16BigEndian(reader.ReadBytes(2)); 26 | result.VersionMinor = BinaryPrimitives.ReadInt16BigEndian(reader.ReadBytes(2)); 27 | result.Unknown1 = reader.ReadBytes(0x18); 28 | do 29 | { 30 | var keyEntry = new SfbKeyEntry(); 31 | keyEntry.Key = Encoding.ASCII.GetString(reader.ReadBytes(0x10)).TrimEnd('\0'); 32 | if (string.IsNullOrEmpty(keyEntry.Key)) 33 | break; 34 | 35 | keyEntry.ValueOffset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); 36 | keyEntry.ValueLength = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); 37 | keyEntry.Unknown = reader.ReadInt64(); 38 | result.KeyEntries.Add(keyEntry); 39 | } while (true); 40 | foreach (var entry in result.KeyEntries) 41 | { 42 | reader.BaseStream.Seek(entry.ValueOffset, SeekOrigin.Begin); 43 | entry.Value = Encoding.ASCII.GetString(reader.ReadBytes(entry.ValueLength)).TrimEnd('\0'); 44 | } 45 | return result; 46 | } 47 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/Utils/MacOS/DiskArbitration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using System.Runtime.Versioning; 5 | 6 | namespace Ps3DiscDumper.Utils.MacOS; 7 | 8 | [SupportedOSPlatform("osx")] 9 | public static partial class DiskArbitration 10 | { 11 | private const string LibraryName = "/System/Library/Frameworks/DiskArbitration.framework/DiskArbitration"; 12 | 13 | public static readonly IntPtr DescriptionMediaKindKey = CoreFoundation.__CFStringMakeConstantString("DAMediaKind"); 14 | 15 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 16 | public static partial IntPtr DASessionCreate(IntPtr allocator); 17 | 18 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 19 | public static unsafe partial void DARegisterDiskAppearedCallback(IntPtr session, IntPtr match, delegate* unmanaged[Cdecl] callback, IntPtr context); 20 | 21 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 22 | public static unsafe partial void DARegisterDiskDisappearedCallback(IntPtr session, IntPtr match, delegate* unmanaged[Cdecl] callback, IntPtr context); 23 | 24 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 25 | public static unsafe partial IntPtr DAUnregisterCallback(IntPtr session, delegate* unmanaged[Cdecl] callback, IntPtr context); 26 | 27 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 28 | public static partial IntPtr DASessionScheduleWithRunLoop(IntPtr session, IntPtr runLoop, IntPtr runLoopMode); 29 | 30 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 31 | public static partial IntPtr DASessionUnscheduleFromRunLoop(IntPtr session, IntPtr runLoop, IntPtr runloopMode); 32 | 33 | [LibraryImport(LibraryName), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] 34 | public static partial IntPtr DADiskGetBSDName(IntPtr disk); 35 | } 36 | -------------------------------------------------------------------------------- /Ps3DiscDumper/DiscKeyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Ps3DiscDumper.Utils; 3 | 4 | namespace Ps3DiscDumper; 5 | 6 | public class DiscKeyInfo 7 | { 8 | public DiscKeyInfo(byte[] encryptedKey, byte[] decryptedKey, string fullPath, KeyType keyType, string keyFileHash) 9 | { 10 | if ((encryptedKey == null || encryptedKey.Length == 0) && (decryptedKey == null || decryptedKey.Length == 0)) 11 | throw new ArgumentException("At least one type of disc key must be provided", nameof(encryptedKey)); 12 | 13 | if (string.IsNullOrEmpty(keyFileHash)) 14 | throw new ArgumentException("Key file hash is required and can not be empty", nameof(keyFileHash)); 15 | 16 | if (decryptedKey == null || decryptedKey.Length == 0) 17 | DecryptedKey = Decrypter.DecryptDiscKey(encryptedKey); 18 | else 19 | DecryptedKey = decryptedKey; 20 | EncryptedKey = encryptedKey; 21 | DecryptedKeyId = DecryptedKey.ToHexString(); 22 | FullPath = fullPath; 23 | KeyType = keyType; 24 | KeyFileHash = keyFileHash; 25 | } 26 | 27 | public readonly byte[] EncryptedKey; 28 | public readonly byte[] DecryptedKey; 29 | public readonly string FullPath; 30 | public readonly KeyType KeyType; 31 | public readonly string DecryptedKeyId; 32 | public readonly string KeyFileHash; 33 | 34 | public override bool Equals(object obj) { 35 | if (ReferenceEquals(null, obj)) return false; 36 | if (ReferenceEquals(this, obj)) return true; 37 | if (obj.GetType() != typeof(DiscKeyInfo)) return false; 38 | return Equals((DiscKeyInfo)obj); 39 | } 40 | 41 | protected bool Equals(DiscKeyInfo other) 42 | { 43 | return string.Equals(DecryptedKeyId, other.DecryptedKeyId) 44 | && string.Equals(KeyFileHash, other.KeyFileHash) 45 | && KeyType == other.KeyType; 46 | } 47 | 48 | public override int GetHashCode() 49 | { 50 | unchecked 51 | { 52 | var hashCode = DecryptedKeyId?.GetHashCode() ?? 0; 53 | hashCode = (hashCode * 397) ^ (KeyFileHash?.GetHashCode() ?? 0); 54 | hashCode = (hashCode * 397) ^ (int)KeyType; 55 | return hashCode; 56 | } 57 | } 58 | 59 | public static bool operator ==(DiscKeyInfo left, DiscKeyInfo right) { return Equals(left, right); } 60 | public static bool operator !=(DiscKeyInfo left, DiscKeyInfo right) { return !Equals(left, right); } 61 | } 62 | 63 | public enum KeyType 64 | { 65 | Ird, 66 | Redump, 67 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/DiscInfo/DiscInfoConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using DiscUtils.Iso9660; 8 | using IrdLibraryClient; 9 | using IrdLibraryClient.IrdFormat; 10 | using Ps3DiscDumper.Utils; 11 | 12 | namespace Ps3DiscDumper.DiscInfo; 13 | 14 | public static class DiscInfoConverter 15 | { 16 | public static async Task ToDiscInfoAsync(this Ird ird, CancellationToken cancellationToken) 17 | { 18 | List fsInfo; 19 | var sectorSize = 2048L; 20 | using (var stream = new MemoryStream()) 21 | { 22 | using (var headerStream = new MemoryStream(ird.Header)) 23 | await using (var gzipStream = new GZipStream(headerStream, CompressionMode.Decompress)) 24 | await gzipStream.CopyToAsync(stream, cancellationToken).ConfigureAwait(false); 25 | stream.Seek(0, SeekOrigin.Begin); 26 | var reader = new CDReader(stream, true, true); 27 | (fsInfo, _) = await reader.GetFilesystemStructureAsync(cancellationToken).ConfigureAwait(false); 28 | sectorSize = reader.ClusterSize; 29 | } 30 | var checksums = new Dictionary>(ird.FileCount); 31 | #if DEBUG 32 | Log.Debug("IRD checksum data:"); 33 | #endif 34 | foreach (var irdFileInfo in ird.Files) 35 | { 36 | if (!checksums.TryGetValue(irdFileInfo.Offset, out var csList)) 37 | checksums[irdFileInfo.Offset] = csList = new(1); 38 | csList.Add(irdFileInfo.Md5Checksum.ToHexString()); 39 | #if DEBUG 40 | Log.Debug($"{irdFileInfo.Offset,8} (0x{irdFileInfo.Offset:x8}): {irdFileInfo.Md5Checksum.ToHexString()}"); 41 | #endif 42 | } 43 | return new() 44 | { 45 | ProductCode = ird.ProductCode, 46 | DiscVersion = ird.GameVersion, 47 | DiscKeyRawData = ird.Data1.ToHexString(), 48 | DiscKey = Decrypter.DecryptDiscKey(ird.Data1).ToHexString(), 49 | Files = fsInfo.ToDictionary( 50 | f => f.TargetFileName, 51 | f => new FileInfo 52 | { 53 | Offset = f.StartSector * sectorSize, 54 | Size = f.SizeInBytes, 55 | Hashes = new() 56 | { 57 | ["MD5"] = checksums[f.StartSector], 58 | } 59 | }) 60 | }; 61 | } 62 | } -------------------------------------------------------------------------------- /IrdLibraryClient/IrdClientFlexby420.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Net.Http.Json; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using System.Text.RegularExpressions; 6 | using IrdLibraryClient.Compression; 7 | 8 | namespace IrdLibraryClient; 9 | 10 | public partial class IrdClientFlexby420: IrdClient 11 | { 12 | public override Uri BaseUri { get; } = new("https://flexby420.github.io/playstation_3_ird_database/all.json"); 13 | protected override Regex IrdFilename => throw new NotImplementedException(); 14 | private Uri BaseDownloadUri { get; } = new("https://github.com/FlexBy420/playstation_3_ird_database/raw/main/"); 15 | 16 | [DebuggerDisplay("{Link}", Name="{Title}")] 17 | public class IrdInfo 18 | { 19 | public string Title { get; set; } = null!; 20 | public string? FwVer { get; set; } 21 | public string? GameVer { get; set; } 22 | public string? AppVer { get; set; } 23 | public string Link { get; set; } = null!; 24 | } 25 | 26 | [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseLower)] 27 | [JsonSerializable(typeof(Dictionary))] 28 | private partial class IrdInfoSerializer : JsonSerializerContext; 29 | 30 | public override async Task> GetFullListAsync(CancellationToken cancellationToken) 31 | { 32 | try 33 | { 34 | try 35 | { 36 | var data = await Client.GetFromJsonAsync(BaseUri, IrdInfoSerializer.Default.DictionaryStringIrdInfoArray, cancellationToken).ConfigureAwait(false); 37 | if (data is null or { Count: 0 }) 38 | return []; 39 | 40 | var result = new List<(string productCode, string irdFilename)>(5000); 41 | foreach (var (productCode, irdInfoList) in data) 42 | foreach (var irdInfo in irdInfoList) 43 | result.Add((productCode, irdInfo.Link)); 44 | return result; 45 | } 46 | catch (Exception e) 47 | { 48 | Log.Warn(e, "Failed to make API call to IRD Library"); 49 | return []; 50 | } 51 | } 52 | catch (Exception e) 53 | { 54 | Log.Error(e); 55 | return []; 56 | } 57 | } 58 | 59 | protected override string GetDownloadLink(string irdFilename) 60 | { 61 | var builder = new UriBuilder(BaseDownloadUri); 62 | builder.Path = Path.Combine(builder.Path, irdFilename); 63 | return builder.ToString(); 64 | } 65 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PS3 Disc Dumper 2 | =============== 3 | This is a small utility to make decrypted copies of the PS3 game discs, suitable for use in emulators. 4 | 5 | It does require a [compatible blu-ray drive](https://rpcs3.net/quickstart#dumping_drives) and existence of a matching [disc key](http://www.psdevwiki.com/ps3/Bluray_disc#IRD_file) to work. 6 | 7 | Requirements 8 | ============ 9 | * Compatible blu-ray drive 10 | * Disc must have decryption key, either in redump database or in the IRD Library 11 | * For binary release you might need to install .NET prerequisites 12 | * See `Supported OS versions` and `Dependencies` sections in [documentation](https://learn.microsoft.com/en-us/dotnet/core/install/) 13 | 14 | How to use 15 | ========== 16 | 1. Put `ps3-disc-dumper` executable in any folder 17 | * On Linux make the file executable (via `Properties > Permissions` or via console command `$ chmod +x ./ps3-disc-dumper`) 18 | 2. Start the dumper 19 | 3. Follow the instructions 20 | * On Linux you may need to confirm auto-mount or to mount the disc manually (either through file manager or via console command `$ mount`) 21 | 22 | By default all files will be copied in the folder where the dumper is, you can select different location in `Settings`. 23 | 24 | If you have custom key or IRD file, you can put it in local cache (see `Settings` for exact location). 25 | 26 | Logs can be found in `Settings` at the bottom. 27 | 28 | Screenshots 29 | =========== 30 | Windows 11 31 | 32 | 33 | 34 | 35 | Screenshot running on Windows 11 36 | 37 | 38 | Fedora 39 39 | 40 | 41 | 42 | 43 | Screenshot running on Fedora 38 44 | 45 | 46 | Building 47 | ======== 48 | * To compile the solution, you will need to have [.NET 10.0 SDK](https://www.microsoft.com/net/download) installed on your machine. 49 | * It is recommended to use [JetBrains Raider](https://www.jetbrains.com/rider/), [Visual Studio](https://visualstudio.microsoft.com/), or [VS Code](https://code.visualstudio.com/) for development. 50 | * On Linux select the Linux configuration to prevent various issues due to Windows-specific dependencies. 51 | * You can build for both Windows and Linux on Windows (you can test Linux build with WSL2 or with a Linux VM). 52 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | *.sh text eol=lf 65 | -------------------------------------------------------------------------------- /UI.Avalonia/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Avalonia; 4 | using Avalonia.Controls; 5 | using Avalonia.Interactivity; 6 | using Avalonia.LogicalTree; 7 | using Avalonia.Media; 8 | using Avalonia.Platform; 9 | using Avalonia.Threading; 10 | using Avalonia.VisualTree; 11 | using IrdLibraryClient; 12 | using UI.Avalonia.Utils; 13 | using UI.Avalonia.Utils.ColorPalette; 14 | using UI.Avalonia.ViewModels; 15 | 16 | namespace UI.Avalonia.Views; 17 | 18 | public partial class MainWindow : Window 19 | { 20 | public MainWindow() 21 | { 22 | InitializeComponent(); 23 | #if DEBUG 24 | this.AttachDevTools(); 25 | #endif 26 | } 27 | 28 | public override void Show() 29 | { 30 | base.Show(); 31 | App.OnThemeChanged(this, EventArgs.Empty); 32 | App.OnPlatformColorsChanged(this, PlatformSettings?.GetColorValues() ?? new PlatformColorValues 33 | { 34 | AccentColor1 = Color.Parse(ThemeConsts.BrandColor), 35 | AccentColor2 = Color.Parse(ThemeConsts.BrandColor), 36 | AccentColor3 = Color.Parse(ThemeConsts.BrandColor), 37 | }); 38 | } 39 | 40 | private void OnLoaded(object? sender, RoutedEventArgs e) 41 | { 42 | if (!IsExtendedIntoWindowDecorations) 43 | { 44 | TitleButtons.Margin = new(0, 6, 4, 0); 45 | var buttons = TitleButtons.GetSelfAndVisualDescendants().OfType 88 | 89 | 90 | 91 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /UI.Avalonia/Utils/WinRTInterop/NativeWinRTMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Threading; 4 | using MicroCom.Runtime; 5 | 6 | namespace UI.Avalonia.Utils.WinRTInterop; 7 | 8 | internal static class NativeWinRTMethods 9 | { 10 | [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = false)] 11 | internal static extern unsafe IntPtr WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, int length); 12 | 13 | internal static IntPtr WindowsCreateString(string sourceString) 14 | => WindowsCreateString(sourceString, sourceString.Length); 15 | 16 | [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll", CallingConvention = CallingConvention.StdCall)] 17 | internal static extern unsafe char* WindowsGetStringRawBuffer(IntPtr hstring, uint* length); 18 | 19 | [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = false)] 20 | internal static extern unsafe void WindowsDeleteString(IntPtr hString); 21 | 22 | [DllImport("Windows.UI.Composition", EntryPoint = "DllGetActivationFactory", CallingConvention = CallingConvention.StdCall, PreserveSig = false)] 23 | private extern static IntPtr GetWindowsUICompositionActivationFactory(IntPtr activatableClassId); 24 | 25 | internal static IActivationFactory GetWindowsUICompositionActivationFactory(string className) 26 | {//"Windows.UI.Composition.Compositor" 27 | var s = WindowsCreateString(className); 28 | var factory = GetWindowsUICompositionActivationFactory(s); 29 | return MicroComRuntime.CreateProxyFor(factory, true); 30 | } 31 | 32 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 33 | delegate int GetActivationFactoryDelegate(IntPtr classId, out IntPtr ppv); 34 | 35 | internal static T CreateInstance(string fullName) where T : IUnknown 36 | { 37 | var s = WindowsCreateString(fullName); 38 | EnsureRoInitialized(); 39 | var pUnk = RoActivateInstance(s); 40 | using var unk = MicroComRuntime.CreateProxyFor(pUnk, true); 41 | WindowsDeleteString(s); 42 | return MicroComRuntime.QueryInterface(unk); 43 | } 44 | 45 | internal static TFactory CreateActivationFactory(string fullName) where TFactory : IUnknown 46 | { 47 | var s = WindowsCreateString(fullName); 48 | EnsureRoInitialized(); 49 | var guid = MicroComRuntime.GetGuidFor(typeof(TFactory)); 50 | var pUnk = RoGetActivationFactory(s, ref guid); 51 | using var unk = MicroComRuntime.CreateProxyFor(pUnk, true); 52 | WindowsDeleteString(s); 53 | return MicroComRuntime.QueryInterface(unk); 54 | } 55 | 56 | internal enum DISPATCHERQUEUE_THREAD_APARTMENTTYPE 57 | { 58 | DQTAT_COM_NONE = 0, 59 | DQTAT_COM_ASTA = 1, 60 | DQTAT_COM_STA = 2 61 | }; 62 | 63 | internal enum DISPATCHERQUEUE_THREAD_TYPE 64 | { 65 | DQTYPE_THREAD_DEDICATED = 1, 66 | DQTYPE_THREAD_CURRENT = 2, 67 | }; 68 | 69 | [StructLayout(LayoutKind.Sequential)] 70 | internal struct DispatcherQueueOptions 71 | { 72 | public int dwSize; 73 | 74 | [MarshalAs(UnmanagedType.I4)] 75 | public DISPATCHERQUEUE_THREAD_TYPE threadType; 76 | 77 | [MarshalAs(UnmanagedType.I4)] 78 | public DISPATCHERQUEUE_THREAD_APARTMENTTYPE apartmentType; 79 | }; 80 | 81 | [DllImport("coremessaging.dll", PreserveSig = false)] 82 | internal static extern IntPtr CreateDispatcherQueueController(DispatcherQueueOptions options); 83 | 84 | internal enum RO_INIT_TYPE 85 | { 86 | RO_INIT_SINGLETHREADED = 0, // Single-threaded application 87 | RO_INIT_MULTITHREADED = 1, // COM calls objects on any thread. 88 | } 89 | 90 | [DllImport("combase.dll", PreserveSig = false)] 91 | private static extern void RoInitialize(RO_INIT_TYPE initType); 92 | 93 | [DllImport("combase.dll", PreserveSig = false)] 94 | private static extern IntPtr RoActivateInstance(IntPtr activatableClassId); 95 | 96 | [DllImport("combase.dll", PreserveSig = false)] 97 | private static extern IntPtr RoGetActivationFactory(IntPtr activatableClassId, ref Guid iid); 98 | 99 | private static bool s_initialized; 100 | private static void EnsureRoInitialized() 101 | { 102 | if (s_initialized) 103 | return; 104 | RoInitialize(Thread.CurrentThread.GetApartmentState() == ApartmentState.STA ? 105 | RO_INIT_TYPE.RO_INIT_SINGLETHREADED : 106 | RO_INIT_TYPE.RO_INIT_MULTITHREADED); 107 | s_initialized = true; 108 | } 109 | } 110 | 111 | internal class HStringInterop : IDisposable 112 | { 113 | private IntPtr _s; 114 | private readonly bool _owns; 115 | 116 | public HStringInterop(string? s) 117 | { 118 | _s = s == null ? IntPtr.Zero : NativeWinRTMethods.WindowsCreateString(s); 119 | _owns = true; 120 | } 121 | 122 | public HStringInterop(IntPtr str, bool owns = false) 123 | { 124 | _s = str; 125 | _owns = owns; 126 | } 127 | 128 | public IntPtr Handle => _s; 129 | 130 | public unsafe string? Value 131 | { 132 | get 133 | { 134 | if (_s == IntPtr.Zero) 135 | return null; 136 | 137 | uint length; 138 | var buffer = NativeWinRTMethods.WindowsGetStringRawBuffer(_s, &length); 139 | return new string(buffer, 0, (int) length); 140 | } 141 | } 142 | 143 | public void Dispose() 144 | { 145 | if (_s != IntPtr.Zero && _owns) 146 | { 147 | NativeWinRTMethods.WindowsDeleteString(_s); 148 | _s = IntPtr.Zero; 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /Ps3DiscDumper/SettingsProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | using System.Xml.Linq; 7 | using IrdLibraryClient; 8 | using SpecialFolder = System.Environment.SpecialFolder; 9 | 10 | namespace Ps3DiscDumper; 11 | 12 | public static partial class SettingsProvider 13 | { 14 | private static readonly string settingsFolder; 15 | private static readonly string settingsPath; 16 | 17 | static SettingsProvider() 18 | { 19 | try 20 | { 21 | Log.Info("Loading settings…"); 22 | settingsFolder = Path.Combine(Environment.GetFolderPath(SpecialFolder.LocalApplicationData), "ps3-disc-dumper"); 23 | settingsPath = Path.Combine(settingsFolder, "settings.json"); 24 | if (File.Exists(settingsPath)) 25 | { 26 | Log.Debug("Found existing settings file, reading…"); 27 | savedSettings = Read() ?? savedSettings; 28 | } 29 | else 30 | { 31 | if (OperatingSystem.IsWindows()) 32 | { 33 | Log.Debug("No existing settings file was found, trying to import legacy settings…"); 34 | savedSettings = Import() ?? savedSettings; 35 | } 36 | else 37 | { 38 | Log.Debug("Using default settings"); 39 | } 40 | } 41 | Settings = savedSettings; 42 | } 43 | catch (Exception e) 44 | { 45 | Log.Error(e, "Failed to initialize settings"); 46 | } 47 | } 48 | 49 | private static Settings? Read() 50 | { 51 | try 52 | { 53 | using var file = File.Open(settingsPath, FileMode.Open, FileAccess.Read, FileShare.Read); 54 | using var reader = new StreamReader(file); 55 | var settingsContent = reader.ReadToEnd(); 56 | Log.Info($"Current settings: {settingsContent}"); 57 | return JsonSerializer.Deserialize(settingsContent, SettingsSerializer.Default.Settings); 58 | } 59 | catch (Exception e) 60 | { 61 | Log.Error(e, "Failed to initialize settings"); 62 | return null; 63 | } 64 | } 65 | 66 | private static Settings? Import() 67 | { 68 | try 69 | { 70 | if (!OperatingSystem.IsWindows()) 71 | return null; 72 | 73 | var localAppDataPath = Environment.GetFolderPath(SpecialFolder.LocalApplicationData); 74 | var oldRootDir = Path.Combine(localAppDataPath, "UI"); 75 | if (!Directory.Exists(oldRootDir)) 76 | return null; 77 | 78 | var lastUsedConfigPath = Directory 79 | .GetDirectories(oldRootDir, "ps3-disc-dumper*", SearchOption.TopDirectoryOnly) 80 | .Select(d => Path.Combine(d,"1.0.0.0", "user.config")) 81 | .Where(File.Exists) 82 | .MaxBy(f => new FileInfo(f).LastWriteTime); 83 | if (lastUsedConfigPath is null) 84 | return null; 85 | 86 | Log.Info($@"Importing old config from %LocalAppData%\{Path.GetRelativePath(localAppDataPath, lastUsedConfigPath)}"); 87 | var xml = XDocument.Load(lastUsedConfigPath); 88 | if (xml.Root?.Element("userSettings")?.FirstNode is not XElement settingsNode) 89 | return null; 90 | 91 | var template = (string)settingsNode.Descendants("setting") 92 | .FirstOrDefault(el => (string)el.Attribute("name") == "DumpNameTemplate")? 93 | .Element("value"); 94 | var output = (string)settingsNode.Descendants("setting") 95 | .FirstOrDefault(el => (string)el.Attribute("name") == "OutputDir")? 96 | .Element("value"); 97 | var ird = (string)settingsNode.Descendants("setting") 98 | .FirstOrDefault(el => (string)el.Attribute("name") == "IrdDir")? 99 | .Element("value"); 100 | var result = new Settings(); 101 | if (template is { Length: > 0 }) 102 | result = result with { DumpNameTemplate = template }; 103 | if (output is { Length: > 0 } && Directory.Exists(output)) 104 | result = result with { OutputDir = output }; 105 | if (ird is { Length: > 0 } && Directory.Exists(ird)) 106 | result = result with { IrdDir = ird }; 107 | var newSettingsContent = JsonSerializer.Serialize(result, SettingsSerializer.Default.Settings); 108 | Log.Info($"Imported settings: {newSettingsContent}"); 109 | return result; 110 | } 111 | catch (Exception e) 112 | { 113 | Log.Error(e, "Failed to import old settings file"); 114 | return null; 115 | } 116 | } 117 | 118 | public static void Save() 119 | { 120 | var tmp = Settings; 121 | if (tmp.Equals(savedSettings)) 122 | return; 123 | 124 | try 125 | { 126 | if (!Directory.Exists(settingsFolder)) 127 | Directory.CreateDirectory(settingsFolder); 128 | 129 | var settingsContent = JsonSerializer.Serialize(tmp, SettingsSerializer.Default.Settings); 130 | Log.Info($"Updated settings: {settingsContent}"); 131 | using var file = File.Open(settingsPath, FileMode.Create, FileAccess.Write, FileShare.Read); 132 | using var writer = new StreamWriter(file); 133 | writer.Write(settingsContent); 134 | writer.Flush(); 135 | file.Flush(); 136 | savedSettings = tmp; 137 | } 138 | catch (Exception e) 139 | { 140 | Log.Error(e, "Failed to save settings"); 141 | } 142 | } 143 | 144 | private static Settings savedSettings = new(); 145 | public static Settings Settings { get; set; } = new(); 146 | 147 | [JsonSourceGenerationOptions(WriteIndented = true)] 148 | [JsonSerializable(typeof(Settings))] 149 | internal partial class SettingsSerializer: JsonSerializerContext; 150 | } 151 | -------------------------------------------------------------------------------- /UI.Avalonia/Views/MainWindow.axaml.macos.cs: -------------------------------------------------------------------------------- 1 | #if MACOS 2 | using System; 3 | using System.IO; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | using System.Runtime.Versioning; 7 | using System.Threading; 8 | using Avalonia; 9 | using Avalonia.Controls.ApplicationLifetimes; 10 | using Avalonia.Threading; 11 | using IrdLibraryClient; 12 | using Ps3DiscDumper.Utils.MacOS; 13 | using UI.Avalonia.ViewModels; 14 | using CF = Ps3DiscDumper.Utils.MacOS.CoreFoundation; 15 | 16 | namespace UI.Avalonia.Views; 17 | 18 | public partial class MainWindow 19 | { 20 | private Thread? diskArbiterThread; 21 | private IntPtr runLoop = IntPtr.Zero; 22 | 23 | partial void OnLoadedPlatform() 24 | { 25 | if (!OperatingSystem.IsMacOS()) 26 | return; 27 | 28 | // Finder opens applications with the root as the working directory. 29 | // In such cases, change it to the .app bundle directory so relative paths will work. 30 | if (Directory.GetCurrentDirectory() == "/") 31 | { 32 | Directory.SetCurrentDirectory(Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", ".."))); 33 | Log.Debug($"Set working directory to: {Directory.GetCurrentDirectory()}"); 34 | } 35 | 36 | diskArbiterThread = new(RunDiskArbiter); 37 | diskArbiterThread.Start(); 38 | } 39 | 40 | partial void OnClosingPlatform() 41 | { 42 | if (!OperatingSystem.IsMacOS()) 43 | return; 44 | 45 | StopDiskArbiter(); 46 | diskArbiterThread?.Join(); 47 | } 48 | 49 | [SupportedOSPlatform("osx")] 50 | private unsafe void RunDiskArbiter() 51 | { 52 | try 53 | { 54 | runLoop = CF.CFRunLoopGetCurrent(); 55 | var cfAllocator = CF.CFAllocatorGetDefault(); 56 | var daSession = DiskArbitration.DASessionCreate(cfAllocator); 57 | var match = CF.CFDictionaryCreate( 58 | cfAllocator, 59 | [DiskArbitration.DescriptionMediaKindKey], 60 | [IOKit.BdMediaClassCfString], 61 | 1, 62 | CF.TypeDictionaryKeyCallBacks, 63 | CF.TypeDictionaryValueCallBacks 64 | ); 65 | DiskArbitration.DARegisterDiskAppearedCallback(daSession, match, &DiskAppeared, IntPtr.Zero); 66 | DiskArbitration.DARegisterDiskDisappearedCallback(daSession, match, &DiskDisappeared, IntPtr.Zero); 67 | DiskArbitration.DASessionScheduleWithRunLoop(daSession, runLoop, CF.RunLoopDefaultMode); 68 | 69 | // Blocks the thread until stopped. 70 | CF.CFRunLoopRun(); 71 | 72 | DiskArbitration.DAUnregisterCallback(daSession, &DiskAppeared, IntPtr.Zero); 73 | DiskArbitration.DAUnregisterCallback(daSession, &DiskDisappeared, IntPtr.Zero); 74 | DiskArbitration.DASessionUnscheduleFromRunLoop(daSession, runLoop, CF.RunLoopDefaultMode); 75 | CF.CFRelease(daSession); 76 | } 77 | catch (Exception e) 78 | { 79 | Log.Error(e, "Failed to run disk arbiter"); 80 | } 81 | } 82 | 83 | [SupportedOSPlatform("osx")] 84 | private void StopDiskArbiter() 85 | { 86 | if (runLoop != IntPtr.Zero) 87 | { 88 | CF.CFRunLoopStop(runLoop); 89 | } 90 | } 91 | 92 | [SupportedOSPlatform("osx")] 93 | [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] 94 | private static void DiskAppeared(IntPtr disk, IntPtr context) 95 | { 96 | try 97 | { 98 | var bsdName = Marshal.PtrToStringAnsi(DiskArbitration.DADiskGetBSDName(disk)); 99 | Log.Debug($"Disk appeared: {bsdName}"); 100 | // Delay before scanning as the drive may not be fully mounted yet. 101 | Thread.Sleep(TimeSpan.FromSeconds(1)); 102 | Dispatcher.UIThread.Post(() => 103 | { 104 | if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime 105 | { 106 | MainWindow.DataContext: MainWindowViewModel 107 | { 108 | CurrentPage: MainViewModel 109 | { 110 | DumpingInProgress: false 111 | } vm and not 112 | { 113 | // still scanning 114 | FoundDisc: true, 115 | DumperIsReady: false 116 | } 117 | } 118 | }) 119 | { 120 | Log.Debug("Scanning for applicable disks"); 121 | vm.ScanDiscsCommand.Execute(null); 122 | } 123 | }, DispatcherPriority.Background); 124 | } 125 | catch (Exception e) 126 | { 127 | Log.Error(e, "Unable to process disk appear event"); 128 | } 129 | } 130 | 131 | [SupportedOSPlatform("osx")] 132 | [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] 133 | private static void DiskDisappeared(IntPtr disk, IntPtr context) 134 | { 135 | try 136 | { 137 | var bsdName = Marshal.PtrToStringAnsi(DiskArbitration.DADiskGetBSDName(disk)); 138 | Log.Debug($"Disk disappeared: {bsdName}"); 139 | Dispatcher.UIThread.Post(() => 140 | { 141 | if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime 142 | { 143 | MainWindow.DataContext: MainWindowViewModel 144 | { 145 | CurrentPage: MainViewModel 146 | { 147 | dumper: { SelectedPhysicalDevice: { Length: > 0 } spd } dumper 148 | } vm 149 | } 150 | } 151 | && spd == $"/dev/r{bsdName}") 152 | { 153 | Log.Debug("Cancelling dump operation"); 154 | dumper.Cts.Cancel(); 155 | vm.ResetViewModelCommand.Execute(null); 156 | } 157 | }, DispatcherPriority.Background); 158 | } 159 | catch (Exception e) 160 | { 161 | Log.Error(e, "Unable to process disk disappear event"); 162 | } 163 | } 164 | } 165 | #endif 166 | -------------------------------------------------------------------------------- /Ps3DiscDumper/DiscKeyProviders/RedumpProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using IrdLibraryClient; 11 | using Ps3DiscDumper.Utils; 12 | 13 | namespace Ps3DiscDumper.DiscKeyProviders; 14 | 15 | public class RedumpProvider : IDiscKeyProvider 16 | { 17 | private static readonly RedumpClient Client = new(); 18 | 19 | public async Task> EnumerateAsync(string discKeyCachePath, string productCode, CancellationToken cancellationToken) 20 | { 21 | var result = new HashSet(); 22 | try 23 | { 24 | var keyStreams = new List(); 25 | var snapshotStream = await Client.GetKeysZipContent(discKeyCachePath, cancellationToken).ConfigureAwait(false); 26 | if (snapshotStream is not null) 27 | { 28 | Log.Info("Using locally cached redump keys snapshot"); 29 | var copy = new MemoryStream(); 30 | snapshotStream.Seek(0, SeekOrigin.Begin); 31 | await snapshotStream.CopyToAsync(copy, cancellationToken).ConfigureAwait(false); 32 | copy.Seek(0, SeekOrigin.Begin); 33 | keyStreams.Add(copy); 34 | } 35 | else 36 | { 37 | 38 | var assembly = Assembly.GetExecutingAssembly(); 39 | var embeddedResources = assembly.GetManifestResourceNames().Where(n => n.Contains("Disc_Keys") || n.Contains("Disc Keys")).ToList(); 40 | if (embeddedResources is { Count: > 0 }) 41 | { 42 | Log.Info("Loading embedded redump keys"); 43 | foreach (var res in embeddedResources) 44 | { 45 | var resStream = assembly.GetManifestResourceStream(res); 46 | keyStreams.Add(resStream); 47 | } 48 | } 49 | else 50 | Log.Warn("No embedded redump keys found"); 51 | } 52 | foreach (var zipStream in keyStreams) 53 | await using (zipStream) 54 | { 55 | if (zipStream.CanSeek) 56 | zipStream.Seek(0, SeekOrigin.Begin); 57 | using var zip = new ZipArchive(zipStream, ZipArchiveMode.Read); 58 | foreach (var zipEntry in zip.Entries.Where(e => e.Name.EndsWith(".dkey", StringComparison.InvariantCultureIgnoreCase) 59 | || e.Name.EndsWith(".key", StringComparison.InvariantCultureIgnoreCase))) 60 | { 61 | await using var keyStream = zipEntry.Open(); 62 | await using var memStream = new MemoryStream(); 63 | await keyStream.CopyToAsync(memStream, cancellationToken).ConfigureAwait(false); 64 | var discKey = memStream.ToArray(); 65 | if (zipEntry.Length > 256 / 8 * 2) 66 | { 67 | Log.Warn($"Disc key size is too big: {discKey} ({zipEntry.FullName})"); 68 | continue; 69 | } 70 | if (discKey.Length > 16) 71 | { 72 | discKey = Encoding.UTF8.GetString(discKey).TrimEnd().ToByteArray(); 73 | } 74 | 75 | try 76 | { 77 | result.Add(new(null, discKey, zipEntry.FullName, KeyType.Redump, discKey.ToHexString())); 78 | } 79 | catch (Exception e) 80 | { 81 | Log.Warn(e, $"Invalid disc key format: {discKey}"); 82 | } 83 | } 84 | } 85 | if (result.Any()) 86 | Log.Info($"Found {result.Count} redump keys"); 87 | else 88 | Log.Warn($"Failed to load any redump keys"); 89 | } 90 | catch (Exception e) 91 | { 92 | Log.Error(e, "Failed to load redump keys"); 93 | } 94 | 95 | Log.Trace("Loading loose cached redump keys"); 96 | var diff = result.Count; 97 | try 98 | { 99 | if (Directory.Exists(discKeyCachePath)) 100 | { 101 | var matchingDiskKeys = Directory.GetFiles(discKeyCachePath, "*.dkey", SearchOption.TopDirectoryOnly) 102 | .Concat(Directory.GetFiles(discKeyCachePath, "*.key", SearchOption.TopDirectoryOnly)); 103 | foreach (var dkeyFile in matchingDiskKeys) 104 | { 105 | try 106 | { 107 | try 108 | { 109 | var discKey = await File.ReadAllBytesAsync(dkeyFile, cancellationToken).ConfigureAwait(false); 110 | if (discKey.Length > 16) 111 | { 112 | try 113 | { 114 | discKey = Encoding.UTF8.GetString(discKey).TrimEnd().ToByteArray(); 115 | } 116 | catch (Exception e) 117 | { 118 | Log.Warn(e, $"Failed to convert {discKey.ToHexString()} from hex to binary"); 119 | } 120 | } 121 | result.Add(new(null, discKey, dkeyFile, KeyType.Redump, discKey.ToString())); 122 | } 123 | catch (InvalidDataException) 124 | { 125 | File.Delete(dkeyFile); 126 | } 127 | catch (Exception e) 128 | { 129 | Log.Warn(e); 130 | } 131 | } 132 | catch (Exception e) 133 | { 134 | Log.Warn(e, e.Message); 135 | } 136 | } 137 | } 138 | } 139 | catch (Exception ex) 140 | { 141 | Log.Warn(ex, "Failed to load redump keys from local cache"); 142 | } 143 | diff = result.Count - diff; 144 | Log.Info($"Found {diff} cached disc keys"); 145 | return result; 146 | } 147 | } -------------------------------------------------------------------------------- /UI.Avalonia/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Avalonia; 4 | using Avalonia.Controls; 5 | using Avalonia.Controls.ApplicationLifetimes; 6 | using Avalonia.Data.Core.Plugins; 7 | using Avalonia.Markup.Xaml; 8 | using Avalonia.Media; 9 | using Avalonia.Platform; 10 | using Avalonia.Styling; 11 | using Ps3DiscDumper; 12 | using Ps3DiscDumper.Utils; 13 | using UI.Avalonia.Utils.ColorPalette; 14 | using UI.Avalonia.ViewModels; 15 | using UI.Avalonia.Views; 16 | 17 | namespace UI.Avalonia; 18 | 19 | public partial class App : Application 20 | { 21 | private static readonly WindowTransparencyLevel[] DesiredTransparencyHints = 22 | [ 23 | WindowTransparencyLevel.Mica, 24 | WindowTransparencyLevel.AcrylicBlur, 25 | WindowTransparencyLevel.None, 26 | ]; 27 | 28 | private readonly Lazy isMicaCapable = new(() => 29 | Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Window w } 30 | && w.ActualTransparencyLevel == WindowTransparencyLevel.Mica 31 | ); 32 | 33 | public override void Initialize() => AvaloniaXamlLoader.Load(this); 34 | 35 | public override void OnFrameworkInitializationCompleted() 36 | { 37 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 38 | { 39 | // Avoid duplicate validations from both Avalonia and the CommunityToolkit. 40 | // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins 41 | DisableAvaloniaDataAnnotationValidation(); 42 | var safeToRun = SecurityEx.IsSafe(desktop.Args); 43 | Window w; 44 | ViewModelBase vm; 45 | if (safeToRun) 46 | { 47 | var mainViewModel = new MainWindowViewModel(); 48 | vm = mainViewModel.CurrentPage; 49 | SetSymbolFont(vm); 50 | w = new MainWindow { DataContext = mainViewModel }; 51 | } 52 | else 53 | { 54 | vm = new ErrorStubViewModel(); 55 | SetSymbolFont(vm); 56 | w = new ErrorStub { DataContext = vm }; 57 | } 58 | desktop.MainWindow = w; 59 | desktop.MainWindow.Activated += OnActivated; 60 | desktop.MainWindow.Deactivated += OnDeactivated; 61 | desktop.MainWindow.ActualThemeVariantChanged += OnThemeChanged; 62 | if (w.PlatformSettings is { } ps) 63 | ps.ColorValuesChanged += OnPlatformColorsChanged; 64 | 65 | vm.MicaEnabled = isMicaCapable.Value; 66 | vm.AcrylicEnabled = w.ActualTransparencyLevel == WindowTransparencyLevel.AcrylicBlur; 67 | 68 | var systemFonts = FontManager.Current.SystemFonts; 69 | if (systemFonts.TryGetGlyphTypeface("Segoe UI Variable Text", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _)) 70 | w.FontFamily = new("Segoe UI Variable Text"); 71 | else if (systemFonts.TryGetGlyphTypeface("Segoe UI", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _)) 72 | w.FontFamily = new("Segoe UI"); 73 | } 74 | base.OnFrameworkInitializationCompleted(); 75 | } 76 | 77 | private static void SetSymbolFont(ViewModelBase vm) 78 | { 79 | if (OperatingSystem.IsMacOS()) 80 | return; // TryGetGlyphTypeface always returns true but yields a fallback fonts with missing symbols. 81 | 82 | var systemFonts = FontManager.Current.SystemFonts; 83 | if (systemFonts.TryGetGlyphTypeface("Segoe Fluent Icons", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _)) 84 | vm.SymbolFontFamily = new("Segoe Fluent Icons"); 85 | if (systemFonts.TryGetGlyphTypeface("Segoe UI Variable Small", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _)) 86 | vm.SmallFontFamily = new("Segoe UI Variable Small"); 87 | if (systemFonts.TryGetGlyphTypeface("Segoe UI Variable Display", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _)) 88 | vm.LargeFontFamily = new("Segoe UI Variable Display"); 89 | } 90 | 91 | private void OnActivated(object? sender, EventArgs e) 92 | { 93 | if (sender is not Window w) 94 | return; 95 | 96 | if (isMicaCapable.Value && SettingsProvider.Settings.EnableTransparency) 97 | w.TransparencyLevelHint = DesiredTransparencyHints; 98 | } 99 | 100 | private void OnDeactivated(object? sender, EventArgs e) 101 | { 102 | if (sender is not Window { DataContext: MainWindowViewModel vm } w) 103 | return; 104 | 105 | if (isMicaCapable.Value) 106 | w.TransparencyLevelHint = Array.Empty(); 107 | if (w.ActualThemeVariant == ThemeVariant.Light) 108 | vm.CurrentPage.TintColor = ThemeConsts.LightThemeTintColor; 109 | else if (w.ActualThemeVariant == ThemeVariant.Dark) 110 | vm.CurrentPage.TintColor = ThemeConsts.DarkThemeTintColor; 111 | } 112 | 113 | private void DisableAvaloniaDataAnnotationValidation() 114 | { 115 | // Get an array of plugins to remove 116 | var dataValidationPluginsToRemove = 117 | BindingPlugins.DataValidators.OfType().ToArray(); 118 | 119 | // remove each entry found 120 | foreach (var plugin in dataValidationPluginsToRemove) 121 | { 122 | BindingPlugins.DataValidators.Remove(plugin); 123 | } 124 | } 125 | internal static void OnThemeChanged(object? sender, EventArgs e) 126 | { 127 | Window w; 128 | ViewModelBase vm; 129 | if (sender is Window { DataContext: MainWindowViewModel { CurrentPage: {} vm1 } } w1) 130 | (w, vm) = (w1, vm1); 131 | else if (sender is Window { DataContext: ViewModelBase vm2 } w2) 132 | (w, vm) = (w2, vm2); 133 | else 134 | return; 135 | 136 | if (w.ActualThemeVariant == ThemeVariant.Light) 137 | { 138 | vm.TintColor = ThemeConsts.LightThemeTintColor; 139 | vm.TintOpacity = ThemeConsts.LightThemeTintOpacity; 140 | vm.ColorPalette = ThemeConsts.Light; 141 | } 142 | else if (w.ActualThemeVariant == ThemeVariant.Dark) 143 | { 144 | vm.TintColor = ThemeConsts.DarkThemeTintColor; 145 | vm.TintOpacity = ThemeConsts.DarkThemeTintOpacity; 146 | vm.ColorPalette = ThemeConsts.Dark; 147 | } 148 | } 149 | 150 | internal static void OnPlatformColorsChanged(object? sender, PlatformColorValues e) 151 | { 152 | if (Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime 153 | { 154 | MainWindow.DataContext: MainWindowViewModel { CurrentPage: ViewModelBase vm } 155 | }) 156 | return; 157 | 158 | vm.SystemAccentColor = e.AccentColor1.ToString(); 159 | if (SettingsProvider.Settings.PreferSystemAccent) 160 | vm.AccentColor = e.AccentColor1.ToString(); 161 | else 162 | vm.AccentColor = ThemeConsts.BrandColor; 163 | } 164 | } -------------------------------------------------------------------------------- /UI.Avalonia/ViewModels/SettingsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | using Avalonia; 8 | using Avalonia.Controls.ApplicationLifetimes; 9 | using Avalonia.Platform.Storage; 10 | using CommunityToolkit.Mvvm.ComponentModel; 11 | using CommunityToolkit.Mvvm.Input; 12 | using IrdLibraryClient; 13 | using Ps3DiscDumper; 14 | using Ps3DiscDumper.Utils; 15 | 16 | using SpecialFolder = System.Environment.SpecialFolder; 17 | 18 | namespace UI.Avalonia.ViewModels; 19 | 20 | public partial class SettingsViewModel: ViewModelBase 21 | { 22 | public SettingsViewModel() => pageTitle = "Settings"; 23 | 24 | [NotifyPropertyChangedFor(nameof(OutputDirPreview))] 25 | [ObservableProperty] private string outputDir = SettingsProvider.Settings.OutputDir; 26 | [NotifyPropertyChangedFor(nameof(IrdDirPreview))] 27 | [ObservableProperty] private string irdDir = SettingsProvider.Settings.IrdDir; 28 | 29 | public string OutputDirPreview => FormatPathPreview(OutputDir); 30 | public string IrdDirPreview => FormatPathPreview(IrdDir); 31 | 32 | [ObservableProperty] private string templatePreview = FormatPreview(SettingsProvider.Settings.OutputDir, SettingsProvider.Settings.DumpNameTemplate, testItems); 33 | [ObservableProperty] private string dumpNameTemplate = SettingsProvider.Settings.DumpNameTemplate; 34 | [NotifyPropertyChangedFor(nameof(TestProductCode))] 35 | [NotifyPropertyChangedFor(nameof(TestProductCodeLetters))] 36 | [NotifyPropertyChangedFor(nameof(TestProductCodeNumbers))] 37 | [NotifyPropertyChangedFor(nameof(TestTitle))] 38 | [NotifyPropertyChangedFor(nameof(TestRegion))] 39 | [ObservableProperty] 40 | private static NameValueCollection testItems = new() 41 | { 42 | [Patterns.ProductCode] = "BLES02127", 43 | [Patterns.ProductCodeLetters] = "BLES", 44 | [Patterns.ProductCodeNumbers] = "02127", 45 | [Patterns.Title] = "Under Night In-Birth Exe:Late", 46 | [Patterns.Region] = "EU", 47 | }; 48 | public string TestProductCode => testItems[Patterns.ProductCode]!; 49 | public string TestProductCodeLetters => testItems[Patterns.ProductCodeLetters]!; 50 | public string TestProductCodeNumbers => testItems[Patterns.ProductCodeNumbers]!; 51 | public string TestTitle => testItems[Patterns.Title]!; 52 | public string TestRegion => testItems[Patterns.Region]!; 53 | 54 | public bool LogAvailable => File.Exists(Log.LogPath); 55 | public string LogPath => Log.LogPath; 56 | public string LogPathPreview => FormatPathPreview(Log.LogPath); 57 | public string DumperVersion => Dumper.Version; 58 | public string CurrentYear => DateTime.Now.Year.ToString(); 59 | public static string ProjectUrl => "https://github.com/13xforever/ps3-disc-dumper"; 60 | public static string SubmitIssueUrl => $"{ProjectUrl}/issues/new/choose"; 61 | public static string WikiUrlBase => $"{ProjectUrl}/wiki/"; 62 | public static string HybridDiscWikiLink => $"{WikiUrlBase}Hybrid-discs"; 63 | 64 | private static string FormatPathPreview(string path) 65 | { 66 | if (path is "") 67 | return "Couldn't create file due to permission or other issues"; 68 | 69 | if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) 70 | { 71 | var homePath = Environment.GetFolderPath(SpecialFolder.UserProfile); 72 | if (path.StartsWith(homePath)) 73 | return Path.Combine("~", Path.GetRelativePath(homePath, path)); 74 | } 75 | 76 | if (path is "." 77 | || path.StartsWith("./") 78 | || OperatingSystem.IsWindows() && path.StartsWith(@".\")) 79 | return "" + path[1..]; 80 | 81 | return Path.IsPathRooted(path) 82 | ? path 83 | : Path.Combine("", path); 84 | } 85 | 86 | private static string FormatPreview(string outDir, string template, NameValueCollection items) 87 | => PatternFormatter.Format(template.Trim(), items); 88 | 89 | partial void OnOutputDirChanged(string value) 90 | { 91 | TemplatePreview = FormatPreview(value, DumpNameTemplate, TestItems); 92 | SettingsProvider.Settings = SettingsProvider.Settings with { OutputDir = value}; 93 | } 94 | 95 | partial void OnIrdDirChanged(string value) 96 | { 97 | SettingsProvider.Settings = SettingsProvider.Settings with { IrdDir = value }; 98 | } 99 | 100 | partial void OnDumpNameTemplateChanged(string value) 101 | { 102 | TemplatePreview = FormatPreview(OutputDir, value, TestItems); 103 | SettingsProvider.Settings = SettingsProvider.Settings with { DumpNameTemplate = value }; 104 | } 105 | 106 | partial void OnTestItemsChanged(NameValueCollection value) 107 | => TemplatePreview = FormatPreview(OutputDir, DumpNameTemplate, value); 108 | 109 | [RelayCommand] 110 | private void ResetTemplate() => DumpNameTemplate = Settings.DefaultPattern; 111 | 112 | [RelayCommand] 113 | private async Task SelectFolderAsync(string purpose) 114 | { 115 | var curSelectedPath = purpose switch 116 | { 117 | "output" => OutputDir, 118 | "ird" => IrdDir, 119 | _ => throw new NotImplementedException() 120 | }; 121 | var itemsType = purpose switch 122 | { 123 | "output" => "disc dumps", 124 | "ird" => "cached disc keys", 125 | _ => throw new NotImplementedException() 126 | }; 127 | if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime 128 | { 129 | MainWindow.StorageProvider: { } sp 130 | }) 131 | return; 132 | 133 | var curDir = await sp.TryGetFolderFromPathAsync(curSelectedPath).ConfigureAwait(false); 134 | var result = await sp.OpenFolderPickerAsync(new() 135 | { 136 | SuggestedStartLocation = curDir, 137 | Title = "Please select a folder to save " + itemsType, 138 | }); 139 | if (result is [var newDir]) 140 | { 141 | var newPath = newDir.Path.IsFile 142 | ? newDir.Path.LocalPath 143 | : newDir.Path.ToString(); 144 | if (purpose is "output") 145 | OutputDir = newPath; 146 | else if (purpose is "ird") 147 | IrdDir = newPath; 148 | } 149 | } 150 | 151 | [RelayCommand] 152 | private void OpenFolder(string value) 153 | { 154 | var folder = value; 155 | if (File.Exists(folder)) 156 | folder = Path.GetDirectoryName(value); 157 | folder = $"\"{folder}\""; 158 | ProcessStartInfo psi = OperatingSystem.IsWindows() 159 | ? new() { Verb = "open", FileName = folder, UseShellExecute = true, } 160 | : OperatingSystem.IsLinux() 161 | ? new() { FileName = "xdg-open", Arguments = folder, } 162 | : new() { FileName = "open", Arguments = folder, }; 163 | try 164 | { 165 | Process.Start(psi); 166 | } 167 | catch (Exception e) 168 | { 169 | Log.Error(e, "Failed to open log folder"); 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /UI.Avalonia/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 80 | 81 | 91 | 92 | 96 | 97 | 98 | 99 | 100 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | --------------------------------------------------------------------------------