37 Commits
Author SHA1 Message Date
Deterous 8ab6a9d694 Fix file hash for split files with irregular filesize 2024-02-07 09:32:56 +09:00
Deterous bc3df52e75 Cleanup 2024-02-05 09:20:59 +09:00
Deterous 23f8dce272 Bump version, provide build script 2024-02-04 12:09:11 +09:00
Deterous 8e45432aab Throw error for BD-Video hybrid discs without layerbreak 2024-02-03 23:15:13 +09:00
Deterous e9bb0ae00a Allow for output folder argument, bump version 2024-02-01 10:55:23 +09:00
Deterous 4116c56b7d Add verbose flag to IRDKit, improve printing 2024-02-01 10:31:07 +09:00
Deterous e30ac0961a Finalise diff function, bump version 2024-02-01 00:52:35 +09:00
Deterous 1ef9f06e35 Improve diff 2024-01-31 23:41:46 +09:00
Deterous 563b05b44f Bump version 2024-01-31 20:24:34 +09:00
Deterous 07a3a3a7f1 Correct number of files for non-contiguous ISOs 2024-01-31 20:13:42 +09:00
Deterous df9bb8c7f2 Diff command for comparing IRDs 2024-01-31 16:56:35 +09:00
Deterous 1a74e8fed0 Better path handling 2024-01-31 15:03:37 +09:00
Deterous e6f6d1f41a Hash non-contiguous files 2024-01-31 14:44:35 +09:00
Deterous 2fe56e994e Add DiscUtils submodule 2024-01-30 18:19:56 +09:00
Deterous e910312d6e Fix param parsing 2023-12-23 08:56:13 +13:00
Deterous 26bde9d753 Simplify conditionals 2023-12-23 00:07:02 +13:00
Deterous c1fa640e0b Don't assume PARAM.SFO fields exist 2023-12-22 23:53:39 +13:00
Deterous ec6caca3e9 Output file path flag 2023-12-06 13:26:05 +13:00
Deterous 8ccbc8430c Print filename for ISO info, remove trailing comma for JSON 2023-12-06 12:56:31 +13:00
Deterous a036481560 Proper printing 2023-12-05 00:02:09 +13:00
Deterous ba7bacc11c Print all data to file 2023-12-04 23:51:07 +13:00
Deterous 8b82ec3d4e Catch filenotfound errors for non-PS3 ISOs 2023-12-04 23:45:04 +13:00
Deterous 0443ba5b9f Fix info command when not given outpath 2023-12-04 23:40:46 +13:00
Deterous 493c92220a Allow info command to operate in directory 2023-12-04 23:35:52 +13:00
Deterous 602394a182 Automatically detect key files with same name 2023-11-30 13:37:27 +13:00
Deterous 338f7a00b7 Fix issue with manual IRD creation 2023-11-30 12:51:15 +13:00
Deterous 7f9fd1c9d6 Fix info printing 2023-11-30 12:36:24 +13:00
Deterous 4b0d796bd2 Refactor, IRDKit can take directories 2023-11-30 11:05:22 +13:00
Deterous 11ed114507 Hash ISO for searching redump 2023-11-25 03:11:24 +13:00
Deterous 357a8df11b IRDKit minimum functionality 2023-11-25 02:39:47 +13:00
Deterous 652cd4aa52 Generate PIC for hybrid discs using layerbreak 2023-11-25 02:08:29 +13:00
Deterous 43e8517bf0 Refactor for .NET8 2023-11-24 21:46:16 +13:00
Deterous f86a89c04a Move printing into IRDKit 2023-11-16 11:58:27 +13:00
Deterous e665391e47 Start work on CLI tool 2023-11-16 11:45:21 +13:00
Deterous b794ec8f96 Parse PS3_DISC.SFB for redump-style IRDs 2023-11-11 19:50:50 +13:00
Deterous 5a9cce39ae Update README 2023-11-10 22:16:44 +13:00
Deterous 2fcf5fcae3 Print IRD fields, add a READEME 2023-11-10 22:09:00 +13:00
18 changed files with 1805 additions and 499 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "DiscUtils"]
path = DiscUtils
url = https://github.com/Deterous/DiscUtils
-60
View File
@@ -1,60 +0,0 @@
using LibIRD;
using System;
using System.IO;
using System.Text;
namespace BuildIRD
{
internal class Program
{
static void Main()
{
Console.OutputEncoding = Encoding.UTF8;
// Create new reproducible redump-style IRD with a key file
try
{
// Read key from .key file
byte[] discKey = File.ReadAllBytes("./game.key");
IRD ird1 = new ReIRD("./game.iso", discKey);
ird1.Write("./test1.ird");
Console.WriteLine("IRD created using .key:");
// Read IRD and print details to console
IRD ird = IRD.Read("./test1.ird");
Console.WriteLine("IRD Version: " + ird.Version);
Console.WriteLine("Title ID: " + ird.TitleID);
Console.WriteLine("Title: " + ird.Title);
Console.WriteLine("System Version: " + ird.SystemVersion);
Console.WriteLine("Game Version: " + ird.GameVersion);
Console.WriteLine("App Version: " + ird.AppVersion);
}
catch (FileNotFoundException e)
{
Console.WriteLine("File not found: " + e.FileName);
}
// Create new reproducible redump-style IRD with a GetKey log
try
{
IRD ird2 = new ReIRD("./game.iso", "./log.getkey.log");
ird2.Write("./test2.ird");
Console.WriteLine("IRD created using .getkey.log:");
// Read IRD and print details to console
IRD ird = IRD.Read("./test2.ird");
Console.WriteLine("IRD Version: " + ird.Version);
Console.WriteLine("Title ID: " + ird.TitleID);
Console.WriteLine("Title: " + ird.Title);
Console.WriteLine("System Version: " + ird.SystemVersion);
Console.WriteLine("Game Version: " + ird.GameVersion);
Console.WriteLine("App Version: " + ird.AppVersion);
}
catch (FileNotFoundException e)
{
Console.WriteLine("File not found: " + e.FileName);
}
}
}
}
-14
View File
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<Version>0.1</Version>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LibIRD\LibIRD.csproj" />
</ItemGroup>
</Project>
Submodule
+1
Submodule DiscUtils added at 299030c20b
+37
View File
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<OutputType>Exe</OutputType>
<TargetName>irdkit</TargetName>
<AssemblyName>irdkit</AssemblyName>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>0.4.1</Version>
<!-- Package Properties -->
<Authors>Deterous</Authors>
<Description>Library for ISO Rebuild Data</Description>
<Copyright>Copyright (c) Deterous 2023-2024</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/Deterous/LibIRD</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>ps3 iso ird redump</PackageTags>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="./README.md" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LibIRD\LibIRD.csproj" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Core\DiscUtils.Core.csproj" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Iso9660\DiscUtils.Iso9660.csproj" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="SabreTools.RedumpLib" Version="1.3.2" />
</ItemGroup>
</Project>
+939
View File
@@ -0,0 +1,939 @@
using CommandLine;
using DiscUtils;
using DiscUtils.Iso9660;
using LibIRD;
using SabreTools.RedumpLib.Web;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Hashing;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace IRDKit
{
internal class Program
{
#region Options
/// <summary>
/// IRD Creation command
/// </summary>
[Verb("create", HelpText = "Create an IRD from an ISO")]
public class CreateOptions
{
[Value(0, Required = true, HelpText = "Path to an ISO file, or directory of ISO files")]
public IEnumerable<string> ISOPath { get; set; }
[Option('o', "output", HelpText = "Path to the IRD file to be created (will overwrite)")]
public string IRDPath { get; set; }
[Option('b', "layerbreak", HelpText = "Layerbreak value in bytes (use with BD-Video hybrid discs). Default: 12219392")]
public long? Layerbreak { get; set; }
[Option('k', "key", HelpText = "Hexadecimal representation of the disc key")]
public string Key { get; set; }
[Option('l', "getkey-log", HelpText = "Path to a .getkey.log file")]
public string GetKeyLog { get; set; }
[Option('f', "key-file", HelpText = "Path to a redump .key file")]
public string KeyFile { get; set; }
[Option('r', "recurse", HelpText = "Recurse through all subdirectories and generate IRDs for all ISOs")]
public bool Recurse { get; set; }
[Option('v', "verbose", HelpText = "Print more information during IRD creation")]
public bool Verbose { get; set; }
}
/// <summary>
/// IRD or ISO information command
/// </summary>
[Verb("info", HelpText = "Print information from an IRD or ISO")]
public class InfoOptions
{
[Value(0, Required = true, HelpText = "Path to an IRD or ISO file, or directory of IRD and/or ISO files")]
public IEnumerable<string> InPath { get; set; }
[Option('o', "output", HelpText = "Path to the text or json file to be created (will overwrite)")]
public string OutPath { get; set; }
[Option('j', "json", HelpText = "Print IRD or ISO information as a JSON object")]
public bool Json { get; set; }
[Option('r', "recurse", HelpText = "Recurse through all subdirectories and print information for all ISOs and IRDs")]
public bool Recurse { get; set; }
}
/// <summary>
/// IRD diff command
/// </summary>
[Verb("diff", HelpText = "Compare two IRDs and print their differences")]
public class DiffOptions
{
[Value(0, Required = true, HelpText = "Path to the first IRD to compare against")]
public string InPath1 { get; set; }
[Value(1, Required = true, HelpText = "Path to the second IRD file to compare")]
public string InPath2 { get; set; }
[Option('o', "output", HelpText = "Path to the text or json file to be created (will overwrite)")]
public string OutPath { get; set; }
}
#endregion
#region Program
/// <summary>
/// Parse command line arguments
/// </summary>
/// <param name="args">Command line arguments</param>
public static void Main(string[] args)
{
// Ensure console prints foreign characters properly
Console.OutputEncoding = Encoding.UTF8;
// Parse arguments
var result = Parser.Default.ParseArguments<CreateOptions, InfoOptions, DiffOptions>(args).WithParsed(Run);
}
/// <summary>
/// Parse arguments
/// </summary>
/// <param name="args">Command-line arguments</param>
/// <exception cref="ArgumentException"></exception>
private static void Run(object args)
{
switch (args)
{
// Process options from a `create` command
case CreateOptions opt:
// Validate ISO paths
if (opt.ISOPath == null || !opt.ISOPath.Any())
{
Console.Error.WriteLine("Provide a valid ISO path to create an IRD");
return;
}
foreach (string isoPath in opt.ISOPath)
{
// Validate ISO path
if (string.IsNullOrEmpty(isoPath))
continue;
// If directory, search for all ISOs in current directory
if (Directory.Exists(isoPath))
{
// If recurse option enabled, search recursively
IEnumerable<string> isoFiles;
if (opt.Recurse)
{
if (opt.Verbose && isoPath == ".")
Console.WriteLine($"Recursively searching for ISOs in current directory");
else if (opt.Verbose)
Console.WriteLine($"Recursively searching for ISOs in {isoPath}");
isoFiles = Directory.EnumerateFiles(isoPath, "*.iso", SearchOption.AllDirectories);
}
else
{
if (opt.Verbose && isoPath == ".")
Console.WriteLine($"Searching for ISOs in current directory");
else if (opt.Verbose)
Console.WriteLine($"Searching for ISOs in {isoPath}");
isoFiles = Directory.EnumerateFiles(isoPath, "*.iso", SearchOption.TopDirectoryOnly);
}
// Warn if no files are found
if (!isoFiles.Any())
{
if (opt.Recurse)
Console.Error.WriteLine($"No ISOs found in {isoPath} (ensure .iso extension)");
else
Console.Error.WriteLine($"No ISOs found in {isoPath} (ensure .iso extension, or try use -r)");
continue;
}
// Determine output IRD folder
string outputPath = Path.GetDirectoryName(opt.IRDPath);
// Create an IRD file for all ISO files found
foreach (string file in isoFiles)
ISO2IRD(file, irdPath: outputPath, verbose: opt.Verbose);
}
else
{
// Check that given file exists
if (!File.Exists(isoPath))
{
Console.Error.WriteLine($"ISO not found: {isoPath}");
return;
}
string irdPath;
// Save to given output path and filename, if only 1 IRD is being created
if (opt.ISOPath.Count() == 1)
irdPath = ISO2IRD(isoPath, irdPath: opt.IRDPath, hexKey: opt.Key, keyPath: opt.KeyFile, getKeyLog: opt.GetKeyLog, layerbreak: opt.Layerbreak, verbose: opt.Verbose);
// Save to given output path, if more than 1 IRD is being created
else
irdPath = ISO2IRD(isoPath, irdPath: Path.GetDirectoryName(opt.IRDPath), verbose: opt.Verbose);
if (irdPath != null)
Console.WriteLine($"IRD saved to {irdPath}");
}
}
break;
// Process options from an `info` command
case InfoOptions opt:
// Validate required parameter
if (opt.InPath == null || !opt.InPath.Any())
{
Console.Error.WriteLine("Provide a valid ISO or IRD path to print info about");
return;
}
// Clear the output file path if it exists
if (opt.OutPath != null && opt.OutPath != "")
File.Delete(opt.OutPath);
foreach (string filePath in opt.InPath)
{
// Validate path
if (string.IsNullOrEmpty(filePath))
continue;
// If directory, search for all ISOs in current directory
if (Directory.Exists(filePath))
{
// If recurse option enabled, search recursively
IEnumerable<string> irdFiles;
IEnumerable<string> isoFiles;
if (opt.Recurse)
{
if (filePath == ".")
Console.WriteLine($"Recursively searching for IRDs and ISOs in current directory...\n");
else
Console.WriteLine($"Recursively searching for IRDs and ISOs in {filePath}...\n");
irdFiles = Directory.EnumerateFiles(filePath, "*.ird", SearchOption.AllDirectories);
isoFiles = Directory.EnumerateFiles(filePath, "*.iso", SearchOption.AllDirectories);
}
else
{
if (filePath == ".")
Console.WriteLine($"Searching for IRDs and ISOs in current directory...\n");
else
Console.WriteLine($"Searching for IRDs and ISOs in {filePath}...\n");
irdFiles = Directory.EnumerateFiles(filePath, "*.ird", SearchOption.TopDirectoryOnly);
isoFiles = Directory.EnumerateFiles(filePath, "*.iso", SearchOption.TopDirectoryOnly);
}
// Warn if no files are found
if (!isoFiles.Any() && !irdFiles.Any())
{
Console.Error.WriteLine("No IRDs or ISOs found (ensure .ird and .iso extensions)");
return;
}
// Open JSON object
if (opt.Json)
{
if (opt.OutPath != null && opt.OutPath != "")
File.AppendAllText(opt.OutPath, "{\n");
else
Console.WriteLine('{');
}
// Print info from all IRDs
bool noISO = !isoFiles.Any();
string lastIRD = irdFiles.Last();
foreach (string file in irdFiles)
{
PrintInfo(file, opt.Json, (noISO && file.Equals(lastIRD)), opt.OutPath);
}
// Print info from all ISOs
string lastISO = isoFiles.Last();
foreach (string file in isoFiles)
{
try
{
PrintISO(file, opt.Json, file.Equals(lastISO), opt.OutPath);
}
catch (InvalidFileSystemException)
{
// Not a valid ISO file despite extension, assume file is an IRD
if (!opt.Json)
Console.Error.WriteLine($"{file} is not a valid ISO file\n");
}
}
// Close JSON object
if (opt.Json)
{
if (opt.OutPath != null && opt.OutPath != "")
File.AppendAllText(opt.OutPath, "}\n");
else
Console.WriteLine('}');
}
if (opt.OutPath != null && opt.OutPath != "")
Console.WriteLine($"Info saved to {opt.OutPath}");
}
else
{
// Check that given file exists
if (!File.Exists(filePath))
{
Console.Error.WriteLine($"{filePath} is not a valid file");
continue;
}
// Print info from given file
PrintInfo(filePath, opt.Json, true, opt.OutPath);
if (opt.OutPath != null && opt.OutPath != "")
Console.WriteLine($"Info saved to {opt.OutPath}");
}
}
break;
// Process options from a `diff` command
case DiffOptions opt:
// Validate required parameters
if (opt.InPath1 == null || opt.InPath2 == null || !File.Exists(opt.InPath1) || !File.Exists(opt.InPath2))
{
Console.Error.WriteLine("Provide two paths to IRDs to compare");
return;
}
// Clear the output file path if it exists
if (opt.OutPath != null && opt.OutPath != "")
File.Delete(opt.OutPath);
// Compare the two IRDs
PrintDiff(opt.InPath1, opt.InPath2, opt.OutPath);
if (opt.OutPath != null && opt.OutPath != "")
Console.WriteLine($"Diff saved to {opt.OutPath}");
break;
// Unknown command
default:
break;
}
}
#endregion
#region Functionality
/// <summary>
/// Prints info about a file
/// </summary>
/// <param name="inPath">File to retrieve info from</param>
/// <param name="json">Whether to format output as JSON (true) or plain text (false)</param>
/// <param name="outPath">File to output info to</param>
public static void PrintInfo(string inPath, bool json, bool single = true, string outPath = null)
{
// Check if file is an ISO
bool isISO = String.Compare(Path.GetExtension(inPath), ".iso", StringComparison.OrdinalIgnoreCase) == 0;
if (isISO)
{
try
{
PrintISO(inPath, json, single, outPath);
return;
}
catch (InvalidFileSystemException)
{
// Not a valid ISO file despite extension, try open as IRD
}
}
// Assume it is an IRD file
try
{
if (json)
{
IRD ird = IRD.Read(inPath);
if (outPath != null)
File.AppendAllText(outPath, $"\"{Path.GetFileName(inPath)}\": ");
else
Console.Write($"\"{Path.GetFileName(inPath)}\": ");
ird.PrintJson(outPath, single);
}
else
IRD.Read(inPath).Print(outPath, Path.GetFileName(inPath));
if (json)
return;
return;
}
catch (InvalidDataException)
{
// Not a valid IRD file despite extension, give up
if (json)
return;
if (isISO)
Console.Error.WriteLine($"{inPath} is not a valid ISO file\n");
else
Console.Error.WriteLine($"{inPath} is not a valid IRD file\n");
}
}
/// <summary>
/// Print information about ISO file
/// </summary>
/// <param name="isoPath">Path to ISO file</param>
/// <param name="json">Whether to format output as JSON (true) or plain text (false)</param>
/// <param name="outPath">File to output info to</param>
/// <exception cref="FileNotFoundException"></exception>
/// <exception cref="InvalidFileSystemException"></exception>
public static void PrintISO(string isoPath, bool json, bool single = true, string outPath = null)
{
// Open ISO file for reading
using FileStream fs = new(isoPath, FileMode.Open, FileAccess.Read);
// Validate ISO file stream
if (fs == null || !CDReader.Detect(fs))
{
Console.Error.WriteLine($"{isoPath} is not a valid ISO file");
return;
}
// Create new ISO reader
using CDReader reader = new(fs, true, true);
// Write PS3_DISC.SFB info
try
{
using DiscUtils.Streams.SparseStream s = reader.OpenFile("\\PS3_DISC.SFB", FileMode.Open, FileAccess.Read);
PS3_DiscSFB ps3_DiscSFB = new(s);
if (json)
{
// Begin JSON object
if (json)
{
if (outPath != null)
File.AppendAllText(outPath, $"\"{Path.GetFileName(isoPath)}\": {{\n");
else
Console.WriteLine($"\"{Path.GetFileName(isoPath)}\": {{");
}
// Print PS3_DISC.SFB info
if (outPath != null)
File.AppendAllText(outPath, "\"PS3_DISC.SFB\": ");
else
Console.Write("\"PS3_DISC.SFB\": ");
ps3_DiscSFB.PrintJson(outPath);
if (outPath != null)
File.AppendAllText(outPath, ",\n");
else
Console.WriteLine(',');
}
else
ps3_DiscSFB.Print(outPath, Path.GetFileName(isoPath));
}
catch (FileNotFoundException)
{
if (!json)
Console.Error.WriteLine($"{isoPath} is not a valid PS3 ISO file\n");
return;
}
// Write PARAM.SFO info
try
{
using DiscUtils.Streams.SparseStream s = reader.OpenFile("\\PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read);
ParamSFO paramSFO = new(s);
if (json)
{
if (outPath != null)
File.AppendAllText(outPath, "\"PARAM.SFO\": ");
else
Console.Write("\"PARAM.SFO\": ");
paramSFO.PrintJson(outPath);
}
else
paramSFO.Print(outPath, Path.GetFileName(isoPath));
}
catch (FileNotFoundException)
{
if (!json)
Console.Error.WriteLine($"\\PS3_GAME\\PARAM.SFO not found in {isoPath}\n");
}
// End JSON object
if (json)
{
if (single)
{
if (outPath != null)
File.AppendAllText(outPath, "\n}\n");
else
Console.WriteLine("\n}");
}
else
{
if (outPath != null)
File.AppendAllText(outPath, "\n},\n");
else
Console.WriteLine("\n},");
}
}
}
/// <summary>
/// Prints the differences between two IRD files
/// </summary>
/// <param name="irdPath1">First IRD path to compare against</param>
/// <param name="irdPath2">Second IRD path to compare against</param>
/// <param name="outPath">File to write comparison to, null if print to Console</param>
public static void PrintDiff(string irdPath1, string irdPath2, string outPath = null)
{
// Check they are different IRDs
if (Path.GetFullPath(irdPath1) == Path.GetFullPath(irdPath2))
{
Console.Error.WriteLine("Provide two different IRDs for a diff");
return;
}
// Parse each IRD
IRD IRD1 = IRD.Read(irdPath1);
IRD IRD2 = IRD.Read(irdPath2);
// Build a formatted diff
StringBuilder printText = new();
// Print any version difference
if (IRD1.Version != IRD2.Version)
printText.AppendLine($"Version: {IRD1.Version} vs {IRD2.Version}");
// Print any title ID difference
if (IRD1.TitleID != IRD2.TitleID)
printText.AppendLine($"TitleID: {IRD1.TitleID} vs {IRD2.TitleID}");
// Print any title difference
if (IRD1.Title != IRD2.Title)
printText.AppendLine($"Title: \"{IRD1.Title}\" vs \"{IRD2.Title}\"");
// Print any system version difference
if (IRD1.SystemVersion != IRD2.SystemVersion)
printText.AppendLine($"PUP Version: {IRD1.SystemVersion} vs {IRD2.SystemVersion}");
// Print any disc version difference
if (IRD1.DiscVersion != IRD2.DiscVersion)
printText.AppendLine($"Disc Version: {IRD1.DiscVersion} vs {IRD2.DiscVersion}");
// Print any app version difference
if (IRD1.AppVersion != IRD2.AppVersion)
printText.AppendLine($"App Version: {IRD1.AppVersion} vs {IRD2.AppVersion}");
// Un-gzip the headers to compare them
byte[] header1 = Decompress(IRD1.Header);
byte[] header2 = Decompress(IRD2.Header);
// Print the difference in header length, if not 0
if (header1.Length != header2.Length)
printText.AppendLine($"Header Length: {header1.Length} vs {header2.Length}");
// Print number of bytes that the headers differ by, if not 0
int headerDiff;
if (header1.Length < header2.Length)
headerDiff = header2.Length - header1.Length + header1.Where((x, i) => x != header2[i]).Count();
else
headerDiff = header1.Length - header2.Length + header2.Where((x, i) => x != header1[i]).Count();
if (headerDiff != 0)
printText.AppendLine($"Header: Differs by {headerDiff} bytes");
// Un-gzip the footers to compare them
byte[] footer1 = Decompress(IRD1.Footer);
byte[] footer2 = Decompress(IRD2.Footer);
// Print the difference in footer length, if not 0
if (footer1.Length != footer2.Length)
printText.AppendLine($"Footer Length: {footer1.Length} vs {footer2.Length}");
// Print number of bytes that the footers differ by, if not 0
int footerDiff;
if (footer1.Length < footer2.Length)
footerDiff = footer2.Length - footer1.Length + footer1.Where((x, i) => x != footer2[i]).Count();
else
footerDiff = footer1.Length - footer2.Length + footer2.Where((x, i) => x != footer1[i]).Count();
if (footerDiff != 0)
printText.AppendLine($"Footer: Differs by {footerDiff} bytes");
// Print the difference in number of regions, if not 0
if (IRD1.RegionCount != IRD2.RegionCount)
printText.AppendLine($"Region Count: {IRD1.RegionCount} vs {IRD2.RegionCount}");
// Print any differences in region hashes
int regionCount = IRD2.RegionCount < IRD1.RegionCount ? IRD2.RegionCount : IRD1.RegionCount;
if (regionCount > IRD1.RegionHashes.Length)
regionCount = IRD1.RegionHashes.Length;
if (regionCount > IRD2.RegionHashes.Length)
regionCount = IRD2.RegionHashes.Length;
for (int i = 0; i < regionCount; i++)
{
if (!IRD1.RegionHashes[i].SequenceEqual(IRD2.RegionHashes[i]))
printText.AppendLine($"Region {i} Hash: {Convert.ToHexString(IRD1.RegionHashes[i])} vs {Convert.ToHexString(IRD2.RegionHashes[i])}");
}
// Print the difference in number of files, if not 0
if (IRD1.FileCount != IRD2.FileCount)
printText.AppendLine($"File Count: {IRD1.FileCount} vs {IRD2.FileCount}");
// Print the mismatch file hashes, for each file offset at which they differ
List<long> missingOffsets1 = [];
List<long> missingOffsets2 = [];
for (int i = 0; i < IRD1.FileKeys.Length; i++)
{
int j = Array.FindIndex(IRD2.FileKeys, element => element == IRD1.FileKeys[i]);
if (j == -1)
missingOffsets2.Add(IRD1.FileKeys[i]);
if (j != -1 && !IRD1.FileHashes[i].SequenceEqual(IRD2.FileHashes[j]))
printText.AppendLine($"File Hash at Offset {IRD1.FileKeys[i]}: {Convert.ToHexString(IRD1.FileHashes[i])} vs {Convert.ToHexString(IRD2.FileHashes[j])}");
}
for (int i = 0; i < IRD2.FileKeys.Length; i++)
{
int j = Array.FindIndex(IRD1.FileKeys, element => element == IRD2.FileKeys[i]);
if (j == -1)
missingOffsets1.Add(IRD2.FileKeys[i]);
}
// Print the file offsets that differ
if (missingOffsets1.Count > 0)
printText.AppendLine($"File Offsets not Present in {irdPath1}: {string.Join(", ", missingOffsets1)}");
if (missingOffsets2.Count > 0)
printText.AppendLine($"File Offsets not Present in {irdPath2}: {string.Join(", ", missingOffsets2)}");
// Print any extra config data difference
if (IRD1.ExtraConfig != IRD2.ExtraConfig)
printText.AppendLine($"Extra Config: {IRD1.ExtraConfig:X4} vs {IRD2.ExtraConfig:X4}");
// Print any attachments data difference
if (IRD1.Attachments != IRD2.Attachments)
printText.AppendLine($"Attachments: {IRD1.Attachments:X4} vs {IRD2.Attachments:X4}");
// Print any unique ID difference
if (IRD1.UID != IRD2.UID)
printText.AppendLine($"Unique ID: {IRD1.UID:X8} vs {IRD2.UID:X8}");
// Print any data 1 key difference
if (!IRD1.Data1Key.SequenceEqual(IRD2.Data1Key))
printText.AppendLine($"Data 1 Key: {Convert.ToHexString(IRD1.Data1Key)} vs {Convert.ToHexString(IRD2.Data1Key)}");
// Print any data 2 key difference
if (!IRD1.Data2Key.SequenceEqual(IRD2.Data2Key))
printText.AppendLine($"Data 2 Key: {Convert.ToHexString(IRD1.Data2Key)} vs {Convert.ToHexString(IRD2.Data2Key)}");
// Print any PIC difference
if (!IRD1.PIC.SequenceEqual(IRD2.PIC))
printText.AppendLine($"PIC: {Convert.ToHexString(IRD1.PIC)} vs {Convert.ToHexString(IRD2.PIC)}");
// Write formatted string to file if output path provided, otherwise to console
if (outPath != null)
File.AppendAllText(outPath, printText.ToString());
else
Console.WriteLine(printText.ToString());
}
/// <summary>
/// Creates an IRD file from an ISO file
/// </summary>
/// <param name="isoPath">Path to an ISO file</param>
/// <param name="irdPath">Path to IRD file to be created (optional)</param>
/// <param name="hexKey">Hex string disc key</param>
/// <param name="keyPath">Disc key file (overridden by hex string if present)</param>
/// <param name="getKeyLog">GetKey log file (overridden by disc key or key file if present)</param>
/// <param name="layerbreak">Layerbreak value of disc</param>
public static string ISO2IRD(string isoPath, string irdPath = null, string hexKey = null, string keyPath = null, string getKeyLog = null, long? layerbreak = null, bool verbose = false)
{
// Check file exists
FileInfo iso;
try
{
iso = new(isoPath);
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
if (!iso.Exists)
{
Console.Error.WriteLine($"{nameof(isoPath)} is not a valid file or directory");
return null;
}
// Determine IRD path if only folder given
if (Directory.Exists(irdPath))
irdPath = Path.Combine(irdPath, Path.GetFileName(Path.ChangeExtension(isoPath, ".ird")));
// Determine IRD path if none given
if (irdPath == string.Empty)
irdPath = Path.GetFileName(Path.ChangeExtension(isoPath, ".ird"));
irdPath ??= Path.ChangeExtension(isoPath, ".ird");
// Create new reproducible redump-style IRD with a given hex key
if (hexKey != null)
{
try
{
// Get disc key from hex string
byte[] discKey = Convert.FromHexString(hexKey);
if (discKey == null || discKey.Length != 16)
Console.Error.WriteLine($"{hexKey} is not a valid key, detecting key automatically...");
else
{
Console.WriteLine($"Creating {irdPath} with Key: {hexKey}");
IRD ird1 = new ReIRD(isoPath, discKey, layerbreak);
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
}
catch (ArgumentException e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found, failed to create IRD");
return null;
}
}
// Create new reproducible redump-style IRD with a given key file
if (keyPath != null)
{
try
{
// Read key from .key file
byte[] discKey = File.ReadAllBytes(keyPath);
if (discKey == null || discKey.Length != 16)
Console.Error.WriteLine($"{hexKey} is not a valid key, detecting key automatically...");
else
{
IRD ird1 = new ReIRD(isoPath, discKey, layerbreak);
Console.WriteLine($"Creating {irdPath} with Key: {Convert.ToHexString(discKey)}");
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
}
catch (ArgumentException e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found, failed to create IRD");
return null;
}
}
// Create new reproducible redump-style IRD with a given GetKey log
if (getKeyLog != null)
{
try
{
Console.WriteLine($"Creating {irdPath} with key from: {getKeyLog}");
IRD ird1 = new ReIRD(isoPath, getKeyLog);
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
catch (ArgumentException e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found, failed to create IRD");
return null;
}
}
// No key provided, try search for .key file
string keyfilePath = Path.ChangeExtension(isoPath, ".key");
FileInfo keyFile = new(keyfilePath);
if (keyFile.Exists)
{
// Found .key file, try use it
try
{
// Read key from .key file
byte[] discKey = File.ReadAllBytes(keyPath);
if (discKey == null || discKey.Length != 16)
Console.Error.WriteLine($"{hexKey} is not a valid key, detecting key automatically...");
else
{
IRD ird1 = new ReIRD(isoPath, discKey, layerbreak);
Console.WriteLine($"Creating {irdPath} with Key: {Convert.ToHexString(discKey)}");
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
}
catch (ArgumentException e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found, failed to create IRD");
return null;
}
}
// No key provided, try search for .getkey.log file
string logfilePath = Path.ChangeExtension(isoPath, ".getkey.log");
FileInfo logfile = new(logfilePath);
if (logfile.Exists)
{
// Found .getkey.log file, check it is valid
try
{
Console.WriteLine($"Creating {irdPath} with key from: {getKeyLog}");
IRD ird1 = new ReIRD(isoPath, getKeyLog);
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
catch (ArgumentException e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found, failed to create IRD");
return null;
}
}
// No key provided, try get key from redump.org
if (verbose)
Console.WriteLine("No key provided... Searching for key on redump.org");
// Compute CRC32 hash
byte[] crc32;
using (FileStream fs = File.OpenRead(isoPath))
{
Crc32 hasher = new();
hasher.Append(fs);
crc32 = hasher.GetCurrentHash();
// Change endianness
Array.Reverse(crc32);
}
string crc32_hash = Convert.ToHexString(crc32).ToLower();
// Search for ISO on redump.org
RedumpHttpClient redump = new();
List<int> ids = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + crc32_hash).ConfigureAwait(false).GetAwaiter().GetResult();
int id;
if (ids.Count == 0)
{
Console.Error.WriteLine("ISO not found in redump and no valid key provided, cannot create IRD");
return null;
}
else if (ids.Count > 1)
{
// More than one result for the CRC32 hash, compute SHA1 hash instead
byte[] sha1;
using (FileStream fs = File.OpenRead(isoPath))
{
SHA1 hasher = SHA1.Create();
sha1 = hasher.ComputeHash(fs);
}
string sha1_hash = Convert.ToHexString(sha1).ToLower();
// Search redump.org for SHA1 hash
List<int> ids2 = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + sha1_hash).ConfigureAwait(false).GetAwaiter().GetResult();
if (ids2.Count == 0)
{
Console.Error.WriteLine("ISO not found in redump and no valid key provided, cannot create IRD");
return null;
}
else if (ids2.Count > 1)
{
Console.Error.WriteLine("Cannot automatically get key from redump. Please search redump.org and run again with -k");
return null;
}
id = ids2[0];
}
else
{
// One result found, assume it is the PS3 ISO
id = ids[0];
}
// Download key file from redump.org
byte[] key = redump.GetByteArrayAsync($"http://redump.org/disc/{id}/key").ConfigureAwait(false).GetAwaiter().GetResult();
if (key.Length != 16)
{
Console.Error.WriteLine("Invalid key obtained from redump and no valid key provided, cannot create IRD");
return null;
}
// Create IRD with key from redump
Console.WriteLine($"Creating {irdPath} with Key from redump.org: {Convert.ToHexString(key)}");
try
{
IRD ird = new ReIRD(isoPath, key, layerbreak);
ird.Write(irdPath);
if (verbose)
ird.Print();
return irdPath;
}
catch (ArgumentException e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found, failed to create IRD");
return null;
}
}
#endregion
#region Helper Functions
/// <summary>
/// Decompress a gzipped byte array
/// </summary>
/// <param name="data">Gzipped byte array</param>
/// <returns>Un-gzipped byte array</returns>
static byte[] Decompress(byte[] data)
{
using var compressedStream = new MemoryStream(data);
using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
using var resultStream = new MemoryStream();
zipStream.CopyTo(resultStream);
return resultStream.ToArray();
}
#endregion
}
}
+41
View File
@@ -0,0 +1,41 @@
# How to use IRDKit
IRDKit is a tool that allows direct use of LibIRD functionality from the command line interface.
For full help instructions, run `irdkit help`
## Creating ISOs
For all options, run `irdkit help create`
To create an IRD from an ISO, run `irdkit create game.iso`
Multiple ISOs can be processed at once with `irdkit create game1.iso game2.iso`
Or a whole directory of ISOs can be processed with `irdkit create ./ISO`, or recursively with `irdkit create -r ./ISO`
The IRD will be created in the same folder as the ISO, with the same filename.
A different IRD path and/or filename can be defined with `-o` or `--output=`
### Key
By default, IRDs will be created by pulling keys from redump.org
A key can be manually provided with `-k` or `--key=`
A key file can be provided with `-f game.key` or `--key-file=`
A key from GetKey log file can be used with `-l game.getkey.log` or `--getkey-log=`
### PIC
By default, a PIC will be generated assuming a default layerbreak
A layerbreak value can be provided with `-b` or `--layerbreak=`
A PIC from a GetKey log file can be used with `-l game.getkey.log` or `--getkey-log=`
## Printing info
For all options, run `irdkit help info`
To print info about an ISO or IRD file, run `irdkit info game.iso` or `irdkit info game.ird`
The info can be printed to a file, e.g. `-o out.txt` or `--output=`
The info can be formatted as a JSON with `-j` or `--json`
## Comparing IRDs
For all options, run `irdkit help diff`
To compare two IRDs, run `irdkit diff game1.ird game2.ird`
The comparison can be printed to a file, e.g. `-o out.txt` or `--output=`
+30 -10
View File
@@ -5,9 +5,21 @@ VisualStudioVersion = 17.7.34202.233
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibIRD", "LibIRD\LibIRD.csproj", "{4A80C34B-D4C5-4536-BEAE-46218BC09980}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildIRD", "BuildIRD\BuildIRD.csproj", "{A2FC2D93-1C3E-46E2-9D69-11F9DDA8CFC7}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRDKit", "IRDKit\IRDKit.csproj", "{9060E3AF-E4FB-436F-93A1-5C47389CB88E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintParams", "PrintParams\PrintParams.csproj", "{B5D9C0AD-EA8D-486E-A1D7-ED2C3C7163BC}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A8BB1F9F-FC8E-471C-9647-6FFF91D63BA7}"
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
LICENSE.txt = LICENSE.txt
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils.Core", "DiscUtils\Library\DiscUtils.Core\DiscUtils.Core.csproj", "{9E71F5CD-887D-4A61-8460-9F7E8F110B46}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils.Iso9660", "DiscUtils\Library\DiscUtils.Iso9660\DiscUtils.Iso9660.csproj", "{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils.Streams", "DiscUtils\Library\DiscUtils.Streams\DiscUtils.Streams.csproj", "{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -19,14 +31,22 @@ Global
{4A80C34B-D4C5-4536-BEAE-46218BC09980}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A80C34B-D4C5-4536-BEAE-46218BC09980}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A80C34B-D4C5-4536-BEAE-46218BC09980}.Release|Any CPU.Build.0 = Release|Any CPU
{A2FC2D93-1C3E-46E2-9D69-11F9DDA8CFC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A2FC2D93-1C3E-46E2-9D69-11F9DDA8CFC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2FC2D93-1C3E-46E2-9D69-11F9DDA8CFC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2FC2D93-1C3E-46E2-9D69-11F9DDA8CFC7}.Release|Any CPU.Build.0 = Release|Any CPU
{B5D9C0AD-EA8D-486E-A1D7-ED2C3C7163BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5D9C0AD-EA8D-486E-A1D7-ED2C3C7163BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5D9C0AD-EA8D-486E-A1D7-ED2C3C7163BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5D9C0AD-EA8D-486E-A1D7-ED2C3C7163BC}.Release|Any CPU.Build.0 = Release|Any CPU
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Release|Any CPU.Build.0 = Release|Any CPU
{9E71F5CD-887D-4A61-8460-9F7E8F110B46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E71F5CD-887D-4A61-8460-9F7E8F110B46}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E71F5CD-887D-4A61-8460-9F7E8F110B46}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E71F5CD-887D-4A61-8460-9F7E8F110B46}.Release|Any CPU.Build.0 = Release|Any CPU
{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}.Release|Any CPU.Build.0 = Release|Any CPU
{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+280 -125
View File
@@ -1,5 +1,6 @@
using DiscUtils;
using DiscUtils.Iso9660;
using DiscUtils.Streams;
using System;
using System.IO;
using System.IO.Compression;
@@ -33,32 +34,32 @@ namespace LibIRD
/// IRD file signature
/// </summary>
/// <remarks>"3IRD"</remarks>
private static readonly byte[] Magic = { 0x33, 0x49, 0x52, 0x44 };
private static readonly byte[] Magic = [0x33, 0x49, 0x52, 0x44];
/// <summary>
/// MD5 hash of null
/// </summary>
private static readonly byte[] NullMD5 = new byte[] { 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e };
private static readonly byte[] NullMD5 = [0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e];
/// <summary>
/// AES CBC Encryption Key for Data 1 (Disc Key)
/// </summary>
private static readonly byte[] D1AesKey = { 0x38, 0x0B, 0xCF, 0x0B, 0x53, 0x45, 0x5B, 0x3C, 0x78, 0x17, 0xAB, 0x4F, 0xA3, 0xBA, 0x90, 0xED };
private static readonly byte[] D1AesKey = [0x38, 0x0B, 0xCF, 0x0B, 0x53, 0x45, 0x5B, 0x3C, 0x78, 0x17, 0xAB, 0x4F, 0xA3, 0xBA, 0x90, 0xED];
/// <summary>
/// AES CBC Initial Value for Data 1 (Disc Key)
/// </summary>
private static readonly byte[] D1AesIV = { 0x69, 0x47, 0x47, 0x72, 0xAF, 0x6F, 0xDA, 0xB3, 0x42, 0x74, 0x3A, 0xEF, 0xAA, 0x18, 0x62, 0x87 };
private static readonly byte[] D1AesIV = [0x69, 0x47, 0x47, 0x72, 0xAF, 0x6F, 0xDA, 0xB3, 0x42, 0x74, 0x3A, 0xEF, 0xAA, 0x18, 0x62, 0x87];
/// <summary>
/// AES CBC Encryption Key for Data 2 (Disc ID)
/// </summary>
private static readonly byte[] D2AesKey = { 0x7C, 0xDD, 0x0E, 0x02, 0x07, 0x6E, 0xFE, 0x45, 0x99, 0xB1, 0xB8, 0x2C, 0x35, 0x99, 0x19, 0xB3 };
private static readonly byte[] D2AesKey = [0x7C, 0xDD, 0x0E, 0x02, 0x07, 0x6E, 0xFE, 0x45, 0x99, 0xB1, 0xB8, 0x2C, 0x35, 0x99, 0x19, 0xB3];
/// <summary>
/// AES CBC Initial Value for Data 2 (Disc ID)
/// </summary>
private static readonly byte[] D2AesIV = { 0x22, 0x26, 0x92, 0x8D, 0x44, 0x03, 0x2F, 0x43, 0x6A, 0xFD, 0x26, 0x7E, 0x74, 0x8B, 0x23, 0x93 };
private static readonly byte[] D2AesIV = [0x22, 0x26, 0x92, 0x8D, 0x44, 0x03, 0x2F, 0x43, 0x6A, 0xFD, 0x26, 0x7E, 0x74, 0x8B, 0x23, 0x93];
#endregion
@@ -88,7 +89,7 @@ namespace LibIRD
/// <summary>
/// Extra Config
/// </summary>
/// <remarks>Reserved, usually set to 0x0000</remarks>
/// <remarks>Usually set to 0x0000, set to 0x0001 for redump-style IRDs</remarks>
public ushort ExtraConfig { get; set; } = 0x0000; // Default to zero
/// <summary>
@@ -116,7 +117,6 @@ namespace LibIRD
}
}
private byte[] _discKey;
// TODO: Link Data1Key and Disc Key
/// <summary>
/// D1 key
@@ -217,7 +217,7 @@ namespace LibIRD
/// The same value stored in PARAM.SFO / VERSION
/// </summary>
/// <remarks>5 bytes, ASCII, e.g. "01.20"</remarks>
public string GameVersion { get; private set; }
public string DiscVersion { get; private set; }
/// <summary>
/// The same value stored in PARAM.SFO / APP_VER
@@ -312,7 +312,7 @@ namespace LibIRD
string titleID,
string title,
string sysVersion,
string gameVersion,
string discVersion,
string appVersion,
byte[] header,
byte[] footer,
@@ -329,7 +329,7 @@ namespace LibIRD
TitleID = titleID;
Title = title;
SystemVersion = sysVersion;
GameVersion = gameVersion;
DiscVersion = discVersion;
AppVersion = appVersion;
HeaderLength = (uint)header.Length;
Header = header;
@@ -364,16 +364,16 @@ namespace LibIRD
/// <param name="discKey">Disc Key, byte array of length 16</param>
/// <param name="discID">Disc ID, byte array of length 16</param>
/// <param name="discPIC">Disc PIC, byte array of length 115</param>
public IRD(string isoPath, byte[] discKey, byte[] discID, byte[] discPIC)
/// <param name="redump">True if redump-style IRD (default: false)</param>
public IRD(string isoPath, byte[] discKey, byte[] discID, byte[] discPIC, bool redump = false)
{
// Parse ISO, Disc Key, Disc ID, and PIC
DiscKey = discKey;
GenerateD1(discKey);
GenerateD2(discID);
DiscID = discID;
PIC = discPIC;
// Generate IRD files from ISO
GenerateIRD(isoPath);
GenerateIRD(isoPath, redump);
}
/// <summary>
@@ -381,15 +381,14 @@ namespace LibIRD
/// </summary>
/// <param name="isoPath">Path to the ISO</param>
/// <param name="getKeyLog">Path to the .getkey.log file</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidDataException"></exception>
public IRD(string isoPath, string getKeyLog)
/// <param name="redump">True if redump-style IRD</param>
public IRD(string isoPath, string getKeyLog, bool redump = false)
{
// Parse .getkey.log file
ParseGetKeyLog(getKeyLog);
// Generate IRD files from ISO
GenerateIRD(isoPath);
GenerateIRD(isoPath, redump);
}
#endregion
@@ -406,8 +405,6 @@ namespace LibIRD
private protected static byte[] GenerateD1(byte[] key)
{
// Validate key
if (key == null)
throw new ArgumentNullException(nameof(key));
if (key.Length != 16)
throw new ArgumentException("Disc Key must be a byte array of length 16", nameof(key));
@@ -441,8 +438,6 @@ namespace LibIRD
private protected static byte[] GenerateDiscKey(byte[] d1)
{
// Validate key
if (d1 == null)
throw new ArgumentNullException(nameof(d1));
if (d1.Length != 16)
throw new ArgumentException("Disc Key must be a byte array of length 16", nameof(d1));
@@ -476,10 +471,7 @@ namespace LibIRD
private protected static byte[] GenerateD2(byte[] d2)
{
// Validate id
if (d2 == null)
throw new ArgumentNullException(nameof(d2));
if (d2.Length != 16)
throw new ArgumentException("Disc ID must be a byte array of length 16", nameof(d2));
if (d2.Length != 16) throw new ArgumentException("Disc ID must be a byte array of length 16", nameof(d2));
// Setup AES encryption
using Aes aes = Aes.Create() ?? throw new InvalidOperationException("AES not available. Change your system settings");
@@ -511,8 +503,6 @@ namespace LibIRD
private protected static byte[] GenerateDiscID(byte[] d2)
{
// Validate id
if (d2 == null)
throw new ArgumentNullException(nameof(d2));
if (d2.Length != 16)
throw new ArgumentException("Disc ID must be a byte array of length 16", nameof(d2));
@@ -545,10 +535,7 @@ namespace LibIRD
/// <exception cref="InvalidDataException"></exception>
private protected void ParseGetKeyLog(string getKeyLog)
{
// Validate .getkey.log file path
if (getKeyLog == null)
throw new ArgumentNullException(nameof(getKeyLog));
if (!File.Exists(getKeyLog))
throw new FileNotFoundException(nameof(getKeyLog));
@@ -624,10 +611,11 @@ namespace LibIRD
/// Constructor for generating values from an ISO file
/// </summary>
/// <param name="isoPath">Path to the ISO</param>
/// <param name="redump">True if redump-style IRD</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="FileNotFoundException"></exception>
/// <exception cref="InvalidFileSystemException"></exception>
private protected void GenerateIRD(string isoPath)
private protected void GenerateIRD(string isoPath, bool redump = false)
{
// Parse ISO file as a file stream
using FileStream fs = new FileStream(isoPath, FileMode.Open, FileAccess.Read) ?? throw new FileNotFoundException(isoPath);
@@ -638,16 +626,66 @@ namespace LibIRD
// New ISO Reader from DiscUtils
CDReader reader = new(fs, true, true);
// If generating redump-style IRD
if (redump)
{
// Redump-style IRDs set the lowest bit of ExtraConfig to 1
ExtraConfig |= 0x01;
// Redump-style IRDs use fields from PS3_DISC.SFB
using SparseStream s = reader.OpenFile("PS3_DISC.SFB", FileMode.Open, FileAccess.Read);
// Parse PS3_DISC.SFB file
PS3_DiscSFB ps3_DiscSFB = new(s);
bool titleIDFound = ps3_DiscSFB.Field.TryGetValue("TITLE_ID", out string titleID);
// If a valid TITLE_ID field is present, remove the hyphen to fit into standard IRD file
if (titleIDFound && titleID.Length == 10 && titleID[4] == '-')
TitleID = string.Concat(titleID.AsSpan(0, 4), titleID.AsSpan(5, 5));
// If the version field is present, this is a multi-game disc
// Redump-style IRDs use the VERSION field from PS3_DISC.SFB instead of VERSION from PARAM.SFO
bool discVersionFound = ps3_DiscSFB.Field.TryGetValue("VERSION", out string discVersion);
if (discVersionFound)
DiscVersion = discVersion;
}
// Read PS3 Metadata from PARAM.SFO
using (DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read))
using (SparseStream s = reader.OpenFile("\\PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read))
{
// Parse PARAM.SFO file
ParamSFO paramSFO = new(s);
// Store required values for IRD
TitleID = paramSFO["TITLE_ID"];
Title = paramSFO["TITLE"];
GameVersion = paramSFO["VERSION"];
AppVersion = paramSFO["APP_VER"];
// If PS3_DISC.SFB did not set TitleID, use PARAM.SFO TITLE_ID
if (TitleID == null)
{
bool titleIDFound = paramSFO.Field.TryGetValue("TITLE_ID", out string titleID);
if (titleIDFound)
TitleID = titleID.Length == 9 ? titleID : titleID.PadRight(9, '\0')[..9];
else
TitleID = "\0\0\0\0\0\0\0\0\0";
}
// Try use Title from PARAM.SFO
bool titleFound = paramSFO.Field.TryGetValue("TITLE", out string title);
Title = titleFound ? title : String.Empty;
// If PS3_DISC.SFB did not set DiscVersion, try use PARAM.SFO VERSION
if (DiscVersion == null)
{
bool discVersionFound = paramSFO.Field.TryGetValue("VERSION", out string discVersion);
if (discVersionFound)
DiscVersion = discVersion.Length == 5 ? discVersion : discVersion.PadRight(5, '\0')[..5];
else
DiscVersion = "\0\0\0\0\0";
}
// Try use App Version from PARAM.SFO
bool appVersionFound = paramSFO.Field.TryGetValue("APP_VER", out string appVersion);
if (appVersionFound)
AppVersion = appVersion.Length == 5 ? appVersion : appVersion.PadRight(5, '\0')[..5];
else
AppVersion = "\0\0\0\0\0";
}
// Determine system update version
@@ -674,9 +712,14 @@ namespace LibIRD
// Determine file offsets and hashes
uint fileCount = FileCount;
FileCount = 0;
ProcessFiles(fs, reader, rootDir);
HashFiles(fs, reader, rootDir);
if (FileCount != fileCount)
throw new InvalidFileSystemException("Unexpected ISO filesystem error: ");
{
Console.WriteLine($"{isoPath} contains split files: detected {FileCount} out of {fileCount} expected files");
long[] tempFileKeys = FileKeys;
Array.Resize(ref tempFileKeys, (int)FileCount);
FileKeys = tempFileKeys;
}
Array.Sort(FileKeys, FileHashes);
}
@@ -694,33 +737,22 @@ namespace LibIRD
private void GetSystemVersion(FileStream fs, CDReader reader)
{
// Determine PUP file offset via cluster
DiscUtils.Streams.Range<long, long>[] updateClusters = reader.PathToClusters("\\PS3_UPDATE\\PS3UPDAT.PUP");
if (updateClusters == null || updateClusters.Length <= 0)
{
// File too small for dedicated cluster, try get the offset from the file extents instead
DiscUtils.Streams.StreamExtent[] updateExtents = reader.PathToExtents("\\PS3_UPDATE\\PS3UPDAT.PUP");
if (updateExtents == null || updateExtents.Length <= 0)
throw new InvalidFileSystemException("Unexpected PS3UPDAT.PUP file extent in ISO filestream");
// PS3UPDAT.PUP file begins at start of first extent
UpdateOffset = updateExtents[0].Start;
// Update file ends at the last extent plus its length
UpdateEnd = updateExtents[^1].Start + updateExtents[^1].Length;
}
else
{
// PS3UPDAT.PUP file begins at first byte of dedicated cluster
UpdateOffset = updateClusters[0] != null ? SectorSize * updateClusters[0].Offset : 0;
// Update file ends at the last byte of the last cluster
UpdateEnd = SectorSize * (updateClusters[^1].Offset + updateClusters[^1].Count);
}
Range<long, long>[] updateClusters = reader.PathToClusters("\\PS3_UPDATE\\PS3UPDAT.PUP");
if (updateClusters == null && updateClusters.Length == 0 && updateClusters[0] == null)
throw new InvalidFileSystemException("Invalid file extents for PS3UPDAT.PUP");
// PS3UPDAT.PUP file begins at first byte of dedicated cluster
UpdateOffset = SectorSize * updateClusters[0].Offset;
// Update file ends at the last byte of the last cluster
UpdateEnd = SectorSize * updateClusters[^1].Offset + updateClusters[^1].Count;
// Check PUP file Magic
fs.Seek(UpdateOffset, SeekOrigin.Begin);
byte[] pupMagic = new byte[5];
fs.Read(pupMagic, 0, pupMagic.Length);
// If magic is incorrect, set version to "0000" (unknown)
// If magic is incorrect, set version to all nulls, "\0\0\0\0" (unknown)
if (Encoding.ASCII.GetString(pupMagic) != "SCEUF")
SystemVersion = "0000";
SystemVersion = "\0\0\0\0";
else
{
// Determine location of version string
@@ -748,24 +780,19 @@ namespace LibIRD
private void GetHeader(FileStream fs, CDReader reader)
{
// Determine the extent of the header via cluster (Sector 0 to first data sector)
DiscUtils.Streams.Range<long, long>[] sfbClusters = reader.PathToClusters("\\PS3_DISC.SFB");
if (sfbClusters == null || sfbClusters.Length <= 0)
{
// File too small for dedicated cluster, try get the first sector from the file extents instead
DiscUtils.Streams.StreamExtent[] sfbExtents = reader.PathToExtents("\\PS3_DISC.SFB");
if (sfbExtents == null || sfbExtents.Length <= 0)
throw new InvalidFileSystemException("Unexpected PS3UPDAT.PUP file extent in ISO filestream");
FirstDataSector = sfbExtents[0].Start;
}
else
{
// End of header is at beginning of first byte of dedicated cluster
FirstDataSector = sfbClusters[0] != null ? sfbClusters[0].Offset : 0;
}
Range<long, long>[] sfbClusters = reader.PathToClusters("\\PS3_DISC.SFB");
if (sfbClusters == null && sfbClusters.Length == 0 && sfbClusters[0] == null)
throw new InvalidFileSystemException("Invalid file extents for PS3_DISC.SFB");
// End of header is at beginning of first byte of dedicated cluster
FirstDataSector = sfbClusters[0].Offset;
// Begin a GZip stream to write header to
using MemoryStream headerStream = new();
#if NET6_0_OR_GREATER
using (GZipStream gzs = new(headerStream, CompressionLevel.SmallestSize))
#else
using (GZipStream gzs = new(headerStream, CompressionLevel.Optimal))
#endif
{
// Start reading data from the beginning of the ISO file
fs.Seek(0, SeekOrigin.Begin);
@@ -793,9 +820,13 @@ namespace LibIRD
{
// Begin a GZip stream to write footer to
using MemoryStream footerStream = new();
#if NET6_0_OR_GREATER
using (GZipStream gzs = new(footerStream, CompressionLevel.SmallestSize))
#else
using (GZipStream gzs = new(footerStream, CompressionLevel.Optimal))
#endif
{
// Start reading data from after last file
// Start reading data from after last file (PS3UPDAT.PUP)
fs.Seek(UpdateEnd, SeekOrigin.Begin);
byte[] buf = new byte[SectorSize];
int numBytes = (int)SectorSize;
@@ -891,64 +922,86 @@ namespace LibIRD
/// </summary>
/// <param name="reader"></param>
/// <param name="path"></param>
private void ProcessFiles(FileStream fs, CDReader reader, DiscDirectoryInfo dir)
private void HashFiles(FileStream fs, CDReader reader, DiscDirectoryInfo dir)
{
// Process all files in current directory
foreach (DiscFileInfo fileInfo in dir.GetFiles())
{
string filePath = fileInfo.FullName;
// Try get the first sector from the file extents instead
DiscUtils.Streams.StreamExtent[] fileExtents = reader.PathToExtents(filePath);
if (fileExtents == null || fileExtents.Length <= 0)
throw new InvalidFileSystemException("Unexpected file extent in ISO filestream for " + filePath);
if (fileExtents.Length > 1)
throw new InvalidFileSystemException("Non-contiguous file detected");
long firstByte = fileExtents[0].Start;
int firstSector = (int)(firstByte / 2048);
long fileLength = fileExtents[0].Length;
FileKeys[FileCount] = firstSector;
// Determine the extents of the file via clusters
Range<long, long>[] fileClusters = reader.PathToClusters(filePath);
// If invalid clusters were returned, we can't hash this file
if (fileClusters == null && fileClusters.Length == 0)
throw new InvalidFileSystemException($"Unexpected file extents for {filePath}");
// Determine smallest file offset as first sector
long smallestOffset = fileClusters[0].Offset;
for (int i = 1; i < fileClusters.Length; i++)
{
if (fileClusters[i] == null)
throw new InvalidFileSystemException($"Unexpected file extents for {filePath}");
if (fileClusters[i].Offset < smallestOffset)
smallestOffset = fileClusters[i].Offset;
}
// If already encountered file offset, skip this file
if (Array.Exists(FileKeys, element => element == smallestOffset))
continue;
if (fileClusters.Length > 1)
Console.WriteLine($"Split file detected: {filePath}");
// Add file offset to keys
FileKeys[FileCount] = smallestOffset;
// Determine whether file is in encrypted or decrypted region
bool encrypted = false;
for (int i = RegionCount - 1; i > 0; i--)
{
if (RegionStart[i] <= firstSector)
if (RegionStart[i] <= smallestOffset)
{
encrypted = i % 2 == 1;
break;
}
}
// Start reading data from the beginning of the ISO file
fs.Seek(firstByte, SeekOrigin.Begin);
// Hash each non-contiguous portion of the ISO file
byte[] buf = new byte[SectorSize];
int numBytes;
// Read all data before the first data sector
MD5 md5 = MD5.Create();
for (int i = 0; i < (fileLength / SectorSize); i++)
for (int i = 0; i < fileClusters.Length; i++)
{
numBytes = fs.Read(buf, 0, buf.Length);
// Check that an entire sector was read
if (numBytes < buf.Length)
throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Decrypt sector if necessary
if (encrypted)
buf = DecryptSector(buf, firstSector + i);
// Hash sector
md5.TransformBlock(buf, 0, numBytes, null, 0);
}
// Read remaining partial sector
if (fileLength % SectorSize != 0)
{
numBytes = fs.Read(buf, 0, buf.Length);
// Check that an entire sector was read
if (numBytes < buf.Length)
throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Decrypt partial sector if necessary
if (encrypted)
buf = DecryptSector(buf, firstSector + (int)(fileLength / SectorSize));
// Hash partial sector
md5.TransformBlock(buf, 0, (int)(fileLength % SectorSize), null, 0);
// Start reading data from the beginning of the ISO file
fs.Seek(fileClusters[i].Offset * SectorSize, SeekOrigin.Begin);
int numBytes;
// Read all data before the first data sector
for (int j = 0; j < (fileClusters[i].Count / SectorSize); j++)
{
numBytes = fs.Read(buf, 0, buf.Length);
// Check that an entire sector was read
if (numBytes < buf.Length)
throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Decrypt sector if necessary
if (encrypted)
buf = DecryptSector(buf, (int)fileClusters[i].Offset + j);
// Hash sector
md5.TransformBlock(buf, 0, numBytes, null, 0);
}
// Read remaining partial sector
if (fileClusters[i].Count % SectorSize != 0)
{
numBytes = fs.Read(buf, 0, buf.Length);
// Check that an entire sector was read
if (numBytes < buf.Length)
throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Decrypt partial sector if necessary
if (encrypted)
buf = DecryptSector(buf, (int)fileClusters[i].Offset + (int)(fileClusters[i].Count / SectorSize));
// Hash partial sector
md5.TransformBlock(buf, 0, (int)(fileClusters[i].Count % SectorSize), null, 0);
}
}
// Finalise and store MD5 hash
@@ -960,7 +1013,7 @@ namespace LibIRD
// Recursively process all subfolders of current directory
foreach (DiscDirectoryInfo dirInfo in dir.GetDirectories())
{
ProcessFiles(fs, reader, dirInfo);
HashFiles(fs, reader, dirInfo);
}
}
@@ -1049,10 +1102,10 @@ namespace LibIRD
bw.Write(systemVersionBuf, 0, 4);
// PARAM.SFO / VERSION
byte[] buf = Encoding.ASCII.GetBytes(GameVersion);
byte[] gameVersionBuf = new byte[5];
Array.Copy(buf, 0, gameVersionBuf, 0, buf.Length);
bw.Write(gameVersionBuf, 0, 5);
byte[] buf = Encoding.ASCII.GetBytes(DiscVersion);
byte[] discVersionBuf = new byte[5];
Array.Copy(buf, 0, discVersionBuf, 0, buf.Length);
bw.Write(discVersionBuf, 0, 5);
// PARAM.SFO / APP_VER
buf = Encoding.ASCII.GetBytes(AppVersion);
@@ -1125,7 +1178,11 @@ namespace LibIRD
// Create the IRD file stream
using FileStream fs = new(irdPath, FileMode.Create, FileAccess.Write);
// Create a GZipped IRD file stream
#if NET6_0_OR_GREATER
using GZipStream gzStream = new(fs, CompressionLevel.SmallestSize);
#else
using GZipStream gzStream = new(fs, CompressionLevel.Optimal);
#endif
// Write entire gzipped IRD stream to file
stream.Position = 0;
stream.CopyTo(gzStream);
@@ -1166,7 +1223,7 @@ namespace LibIRD
// Read System, Game, and App Version
string sysVersion = Encoding.ASCII.GetString(br.ReadBytes(4));
string gameVersion = Encoding.ASCII.GetString(br.ReadBytes(5));
string discVersion = Encoding.ASCII.GetString(br.ReadBytes(5));
string appVersion = Encoding.ASCII.GetString(br.ReadBytes(5));
// Read UID (for Version 7)
@@ -1192,7 +1249,7 @@ namespace LibIRD
uint fileCount = br.ReadUInt32();
long[] fileKeys = new long[fileCount];
byte[][] fileHashes = new byte[fileCount][];
for (int i = 0; i < fileCount; i++)
for (int i = 0; i < fileCount; i++)
{
fileKeys[i] = br.ReadInt64();
fileHashes[i] = br.ReadBytes(16);
@@ -1217,7 +1274,7 @@ namespace LibIRD
// Read UID (for Version 8 onwards)
if (version > 7)
uid = br.ReadUInt16();
uid = br.ReadUInt32();
// Read and CRC32 hash
byte[] crc = br.ReadBytes(4);
@@ -1227,7 +1284,7 @@ namespace LibIRD
titleID,
title,
sysVersion,
gameVersion,
discVersion,
appVersion,
header,
footer,
@@ -1242,6 +1299,104 @@ namespace LibIRD
uid);
}
/// <summary>
/// Prints IRD fields to console
/// </summary>
/// <param name="printPath">Optional path to save file to</param>
public void Print(string printPath = null, string irdName = null)
{
// Build string from parameters
StringBuilder printText = new();
if (irdName == null)
printText.AppendLine("IRD Contents:");
else
printText.AppendLine($"IRD Contents: {irdName}");
printText.AppendLine("=============");
// Append IRD fields to string builder
printText.AppendLine($"Magic: {Encoding.ASCII.GetString(Magic)}");
printText.AppendLine($"IRD Version: {Version}");
printText.AppendLine($"Title ID: {TitleID}");
printText.AppendLine($"Title: {Title}");
printText.AppendLine($"PUP Version: {SystemVersion}");
printText.AppendLine($"Disc Version: {DiscVersion}");
printText.AppendLine($"App Version: {AppVersion}");
printText.AppendLine($"Regions: {RegionCount}");
printText.AppendLine($"Files: {FileCount}");
if (ExtraConfig != 0x0000)
printText.AppendLine($"Extra Config: {ExtraConfig:X4}");
if (Attachments != 0x0000)
printText.AppendLine($"Attachments: {Attachments:X4}");
printText.AppendLine($"Unique ID: {UID:X8}");
printText.AppendLine($"Data 1 Key: {Convert.ToHexString(Data1Key)}");
printText.AppendLine($"Data 2 Key: {Convert.ToHexString(Data2Key)}");
printText.AppendLine($"PIC: {Convert.ToHexString(PIC)}");
printText.AppendLine();
if (printPath == null)
{
// Ensure UTF-8 will display properly
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string
Console.Write(printText);
}
else
{
File.AppendAllText(printPath, printText.ToString());
}
}
/// <summary>
/// Prints IRD fields to a json object
/// </summary>
/// <param name="jsonPath">Optionally print to json file</param>
public void PrintJson(string jsonPath = null, bool single = true)
{
// Build string from parameters
StringBuilder json = new();
// Append IRD fields to string builder
json.AppendLine("{");
json.AppendLine($" \"Magic\": \"{Encoding.ASCII.GetString(Magic)}\",");
json.AppendLine($" \"IRD Version\": \"{Version}\",");
json.AppendLine($" \"Title ID\": \"{TitleID}\",");
json.AppendLine($" \"Title\": \"{Title}\",");
json.AppendLine($" \"PUP Version\": \"{SystemVersion}\",");
json.AppendLine($" \"Disc Version\": \"{DiscVersion}\",");
json.AppendLine($" \"App Version\": \"{AppVersion}\",");
json.AppendLine($" \"Regions\": \"{RegionCount}\",");
json.AppendLine($" \"Files\": \"{FileCount}\",");
if (ExtraConfig != 0x0000)
json.AppendLine($" \"Extra Config\": \"{ExtraConfig:X4}\",");
if (Attachments != 0x0000)
json.AppendLine($" \"Attachments\": \"{Attachments:X4}\",");
json.AppendLine($" \"Unique ID\": \"{UID:X8}\",");
json.AppendLine($" \"Data 1 Key\": \"{Convert.ToHexString(Data1Key)}\",");
json.AppendLine($" \"Data 2 Key\": \"{Convert.ToHexString(Data2Key)}\",");
json.AppendLine($" \"PIC\": \"{Convert.ToHexString(PIC)}\"");
if (single)
json.AppendLine("}");
else
json.AppendLine("},");
// If no path given, output to console
if (jsonPath == null)
{
// Ensure UTF-8 will display properly in console
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string to console
Console.Write(json);
}
else
{
// Write to path
File.AppendAllText(jsonPath, json.ToString());
}
}
#endregion
}
}
+18 -9
View File
@@ -2,25 +2,34 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<Version>0.1.0</Version>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>0.4.1</Version>
<PackageOutputPath>../nupkg</PackageOutputPath>
<!-- Package Properties -->
<Authors>Deterous</Authors>
<Description>Library for ISO Rebuild Data</Description>
<Copyright>Copyright (c)2023 Deterous</Copyright>
<RepositoryUrl>https://github.com/Deterous/LibIRD</RepositoryUrl>
<Copyright>Copyright (c) Deterous 2023-2024</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/Deterous/LibIRD/</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>ps3 iso ird</PackageTags>
<PackageTags>ps3 iso ird redump</PackageTags>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<None Include="./README.md" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscUtils.Core" Version="0.16.13" />
<PackageReference Include="DiscUtils.Iso9660" Version="0.16.13" />
<PackageReference Include="DiscUtils.Streams" Version="0.16.13" />
<PackageReference Include="System.IO.Hashing" Version="7.0.0" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Core\DiscUtils.Core.csproj" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Iso9660\DiscUtils.Iso9660.csproj" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Streams\DiscUtils.Streams.csproj" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
</ItemGroup>
</Project>
+164
View File
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
namespace LibIRD
{
/// <summary>
/// PS3_DISC.SFB file parsing
/// </summary>
public class PS3_DiscSFB
{
/// <summary>
/// PS3_DISC.SFB file signature
/// </summary>
/// <remarks>{ 0x2E, 0x53, 0x46, 0x42 }</remarks>
public static readonly string Magic = ".SFB";
/// <summary>
/// PS3_DISC.SFB file version
/// </summary>
/// <remarks>Typically v1, { 0x00, 0x01 }</remarks>
public ushort Version { get; private set; }
/// <summary>
/// A field within the PS3_DISC.SFB file
/// </summary>
/// <remarks>string Key, string Value</remarks>
public Dictionary<string, string> Field { get; private set; }
/// <summary>
/// Constructor using a PARAM.SFO file path
/// </summary>
/// <param name="sfbPath">Full file path to the PS3_DISC.SFB file</param>
public PS3_DiscSFB(string sfbPath)
{
// Read file as a stream, and parse file
using FileStream fs = new(sfbPath, FileMode.Open, FileAccess.Read);
Parse(fs);
}
/// <summary>
/// Parse PS3_DISC.SFB from stream
/// </summary>
/// <param name="sfbStream">SFB file stream</param>
public PS3_DiscSFB(Stream sfbStream)
{
// Parse file stream
Parse(sfbStream);
}
/// <summary>
/// Read fields from PS3_DISC.SFB
/// </summary>
/// <param name="sfbStream">File stream for PS3_DISC.SFB</param>
/// <exception cref="FileLoadException"></exception>
private void Parse(Stream sfbStream)
{
// Read binary stream
using BinaryReader br = new(sfbStream);
// Check file signature is correct
string magic = Encoding.ASCII.GetString(br.ReadBytes(4));
if (magic != PS3_DiscSFB.Magic)
throw new FileLoadException("Unexpected PS3_DISC.SFB file");
// Read SFB file version
byte[] buf = br.ReadBytes(2);
Array.Reverse(buf);
Version = BitConverter.ToUInt16(buf);
// Process all field headers
sfbStream.Seek(0x20, SeekOrigin.Begin);
string field = Encoding.ASCII.GetString(br.ReadBytes(0x10)).Trim('\0');
Field = [];
while (field != null && field != "")
{
// Find location of value
buf = br.ReadBytes(4);
Array.Reverse(buf);
int offset = BitConverter.ToInt32(buf, 0);
buf = br.ReadBytes(4);
Array.Reverse(buf);
int length = BitConverter.ToInt32(buf, 0);
// Access and store value
long pos = sfbStream.Position;
sfbStream.Seek(offset, SeekOrigin.Begin);
Field[field] = Encoding.ASCII.GetString(br.ReadBytes(length)).Trim('\0');
sfbStream.Seek(pos + 8, SeekOrigin.Begin);
// Attempt to read new field
field = Encoding.ASCII.GetString(br.ReadBytes(0x10)).Trim('\0');
}
}
/// <summary>
/// Prints formatted parameters extracted from PS3_DISC.SFB to console
/// </summary>
/// <param name="printPath">Optionally print to text file</param>
public void Print(string printPath = null, string isoName = null)
{
// Build string from parameters
StringBuilder printText = new();
if (isoName != null)
printText.AppendLine($"PS3_DISC.SFB Contents: {isoName}");
else
printText.AppendLine("PS3_DISC.SFB Contents:");
printText.AppendLine("======================");
// Loop through all parameters in PARAM.SFO
foreach (KeyValuePair<string, string> field in Field)
printText.AppendLine(field.Key + ": " + field.Value);
// Blank line
printText.Append(Environment.NewLine);
// If no path given, print to console
if (printPath == null)
{
// Ensure UTF-8 will display properly in console
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string to console
Console.Write(printText);
}
else
{
// Write data to file
File.AppendAllText(printPath, printText.ToString());
}
}
/// <summary>
/// Define JSON options once
/// </summary>
private readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true };
/// <summary>
/// Prints parameters extracted from PS3_DISC.SFB to a json object
/// </summary>
/// <param name="jsonPath">Optionally print to json file</param>
public void PrintJson(string jsonPath = null)
{
// Serialise PS3_Disc.SFB data to a JSON object
string json = JsonSerializer.Serialize(Field, JsonOpts);
// If no path given, output to console
if (jsonPath == null)
{
// Ensure UTF-8 will display properly in console
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string to console
Console.Write(json);
}
else
{
// Write to path
File.AppendAllText(jsonPath, json);
}
}
}
}
+94 -140
View File
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
namespace LibIRD
{
@@ -18,91 +20,14 @@ namespace LibIRD
/// <summary>
/// PARAM.SFO file version
/// </summary>
/// <remarks>Typically { 0x01 0x01 0x00 0x00 } (v1.1)</remarks>
/// <remarks>Typically { 0x01, 0x01, 0x00, 0x00 } (v1.1)</remarks>
public uint Version { get; private set; }
/// <summary>
/// The location of the first byte of the Key Table
/// A field within the PS3_DISC.SFB file
/// </summary>
public uint KeyTableStart { get; private set; }
/// <summary>
/// The location of the first byte of the Data Table
/// </summary>
public uint DataTableStart { get; private set; }
/// <summary>
/// The number of parameters in the table
/// </summary>
public uint ParamCount { get; private set; }
/// <summary>
/// Parameter, a single entry in parameter table
/// </summary>
public class Param
{
/// <summary>
/// Offset of key, relative to KeyTableStart
/// </summary>
public ushort KeyOffset { get; internal set; }
/// <summary>
/// Format of parameter
/// </summary>
/// <remarks>0x0400 is string, 0x0404 is uint</remarks>
public ushort DataFormat { get; internal set; }
/// <summary>
/// Number of bytes used for parameter
/// </summary>
public uint DataLength { get; internal set; }
/// <summary>
/// Total number of bytes for parameter
/// </summary>
/// <remarks>DataTotal - DataLength is padding of 0x00</remarks>
public uint DataTotal { get; internal set; }
/// <summary>
/// offset of parameter, relative to DataTableStart
/// </summary>
public uint DataOffset { get; internal set; }
/// <summary>
/// The name of the parameter
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// The value of the parameter, if it is a string
/// </summary>
public string StringValue { get; internal set; } = null;
/// <summary>
/// The value of the parameter, if it is a UInt32
/// </summary>
public int IntValue { get; internal set; } = 0;
}
/// <summary>
/// The parameters in the table
/// </summary>
/// <remarks><see cref="ParamCount"/> Params in the table</remarks>
public Param[] Params { get; private set; }
/// <summary>
/// String index overloading, gets string value of given key
/// </summary>
/// <param name="key">Parameter to be retreived</param>
/// <returns>The string value of the given key</returns>
public string this[string key]
{
get
{
int index = Array.FindIndex(Params, param => param.Name == key);
return Params[index].StringValue;
}
}
/// <remarks>string Key, string Value</remarks>
public Dictionary<string, string> Field { get; private set; }
/// <summary>
/// Constructor using a PARAM.SFO file stream
@@ -117,7 +42,7 @@ namespace LibIRD
/// <summary>
/// Constructor using a PARAM.SFO file path
/// </summary>
/// <param name="sfoPath">Full file path to the PARAM.SFO</param>
/// <param name="sfoPath">Full file path to the PARAM.SFO file</param>
/// <exception cref="ArgumentNullException"></exception>
public ParamSFO(string sfoPath)
{
@@ -143,97 +68,126 @@ namespace LibIRD
// Check file signature is correct
string magic = Encoding.ASCII.GetString(br.ReadBytes(4));
if (magic != ParamSFO.Magic)
throw new FileLoadException("Not a valid PARAM.SFO file");
throw new FileLoadException("Unexpected PARAM.SFO file");
// Parse header
Version = br.ReadUInt32();
KeyTableStart = br.ReadUInt32();
DataTableStart = br.ReadUInt32();
ParamCount = br.ReadUInt32();
uint keyTableStart = br.ReadUInt32();
uint dataTableStart = br.ReadUInt32();
uint paramCount = br.ReadUInt32();
// Parse parameter metadata
Params = new Param[ParamCount];
for (int i = 0; i < ParamCount; i++)
ushort[] keyOffset = new ushort[paramCount];
uint[] dataFormat = new uint[paramCount];
uint[] dataLength = new uint[paramCount];
uint[] dataTotal = new uint[paramCount];
uint[] dataOffset = new uint[paramCount];
for (int i = 0; i < paramCount; i++)
{
Params[i] = new Param
{
KeyOffset = br.ReadUInt16(),
DataFormat = br.ReadUInt16(),
DataLength = br.ReadUInt32(),
DataTotal = br.ReadUInt32(),
DataOffset = br.ReadUInt32()
};
keyOffset[i] = br.ReadUInt16();
dataFormat[i] = br.ReadUInt16();
dataLength[i] = br.ReadUInt32();
dataTotal[i] = br.ReadUInt32();
dataOffset[i] = br.ReadUInt32();
}
// Parse parameters
for (int i = 0; i < ParamCount; i++)
Field = [];
for (int i = 0; i < paramCount; i++)
{
// Move stream to ith key
sfoStream.Position = KeyTableStart + Params[i].KeyOffset;
sfoStream.Position = keyTableStart + keyOffset[i];
// Determine ith key length
uint keyLen = ((i == ParamCount - 1) ? DataTableStart - KeyTableStart : Params[i + 1].KeyOffset)
- Params[i].KeyOffset;
uint keyLen = ((i == paramCount - 1) ? dataTableStart - keyTableStart : keyOffset[i + 1])
- keyOffset[i];
// Read ith key name
Params[i].Name = Encoding.ASCII.GetString(br.ReadBytes((int) keyLen)).TrimEnd('\0');
string key = Encoding.ASCII.GetString(br.ReadBytes((int)keyLen)).TrimEnd('\0');
// Move stream to ith data
sfoStream.Position = DataTableStart + Params[i].DataOffset;
sfoStream.Position = dataTableStart + dataOffset[i];
// Read ith data, based on data format
switch (Params[i].DataFormat)
Field[key] = dataFormat[i] switch
{
case 0x0400: // Non-null-terminated UTF-8 String
Params[i].StringValue = Encoding.UTF8.GetString(br.ReadBytes((int)Params[i].DataLength));
break;
case 0x0402: // Null-terminated UTF-8 String
Params[i].StringValue = Encoding.UTF8.GetString(br.ReadBytes((int)Params[i].DataLength)).TrimEnd('\0');
break;
case 0x0404: // Integer
//if (Params[i].DataLength != 4)
//throw new ArgumentException("Integer parameter not 4 bytes?");
Params[i].IntValue = br.ReadInt32();
break;
default: // Unknown data format, assume null-terminated string
Params[i].StringValue = Encoding.UTF8.GetString(br.ReadBytes((int)Params[i].DataLength)).TrimEnd('\0');
break;
}
// Non-null-terminated UTF-8 String
0x0004 => Encoding.UTF8.GetString(br.ReadBytes((int)dataLength[i])),
// Null-terminated UTF-8 String
0x0204 => Encoding.UTF8.GetString(br.ReadBytes((int)dataLength[i])).TrimEnd('\0'),
// Integer
0x0404 => br.ReadInt32().ToString(),
// Unknown data format, assume null-terminated string
_ => Encoding.UTF8.GetString(br.ReadBytes((int)dataLength[i])).TrimEnd('\0'),
};
}
}
/// <summary>
/// Prints formatted parameters extracted from PARAM.SFO to console
/// </summary>
public void Print()
/// <param name="printPath">Optionally print to text file</param>
public void Print(string printPath = null, string isoName = null)
{
// Build string from parameters
StringBuilder print = new("PARAM.SFO Contents:\n====================\n");
StringBuilder printText = new();
if (isoName != null)
printText.AppendLine($"PARAM.SFO Contents: {isoName}");
else
printText.AppendLine("PARAM.SFO Contents:");
printText.AppendLine("===================");
// Loop through all parameters in PARAM.SFO
for (int i = 0; i < ParamCount; i++)
foreach (KeyValuePair<string, string> field in Field)
printText.AppendLine(field.Key + ": " + field.Value);
// Blank line
printText.Append(Environment.NewLine);
// If no path given, print to console
if (printPath == null)
{
print.Append(Params[i].Name);
print.Append(' ');
for (int j = Params[i].Name.Length; j < 20; j++)
print.Append(' ');
switch (Params[i].DataFormat)
{
case 0x0404:
print.Append(Params[i].IntValue);
break;
default:
print.Append(Params[i].StringValue);
break;
}
print.Append('\n');
// Ensure UTF-8 will display properly in console
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string to console
Console.Write(printText);
}
else
{
// Write data to file
File.AppendAllText(printPath, printText.ToString());
}
}
/// <summary>
/// Define JSON options once
/// </summary>
private readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true };
/// <summary>
/// Prints parameters extracted from PARAM.SFO to a json object
/// </summary>
/// <param name="jsonPath">Optionally print to json file</param>
public void PrintJson(string jsonPath = null)
{
// Serialise PS3_Disc.SFB data to a JSON object
string json = JsonSerializer.Serialize(Field, JsonOpts);
// If no path given, output to console
if (jsonPath == null)
{
// Ensure UTF-8 will display properly in console
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string to console
Console.Write(json);
}
else
{
// Write to path
File.AppendAllText(jsonPath, json);
}
// Ensure UTF-8 will display properly
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string
Console.Write(print);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
## How to use the LibIRD library
LibIRD was originally made for creating reproducible, redump-style IRDs when dumping PS3 discs with [MPF](https://github.com/SabreTools/MPF). If you wish to integrate LibIRD into your own application, the following are some examples.
### Reproducible, redump-style IRDs
The standard way of generating a reproducible, redump-style IRD is with a redump ISO file and a redump key file (e.g. http://redump.org/disc/28721/key/):
```cs
byte[] discKey = File.ReadAllBytes("./game.key");
IRD ird = new ReIRD("./game.iso", discKey);
```
Alternatively, the disc key can be extracted from a [GetKey](https://archive.org/download/GetKeyR2GameOS.7z/GetKey-r2-GameOS.7z) log file obtained from a PS3 (or when dumping using [ManaGunZ](https://github.com/Zarh/ManaGunZ/))
```cs
IRD ird = new ReIRD("./game.iso", "./game.getkey.log");
```
### Custom IRDs
Functionality is also provided for creating IRDs with a custom disc key, disc ID, and PIC:
```cs
byte[] discKey = new byte[16];
byte[] discID = new byte[16];
byte[] PIC = new byte[115];
// Set vars to desired values
IRD ird = new IRD("./game.iso", discKey, discID, discPIC);
```
As before, a GetKey log file can also be used with an ISO to create a custom IRD, with the disc key, disc ID, and PIC all being extracted from the log file (rather than just the key for redump-style IRDs).
```cs
IRD ird = new IRD("./game.iso", "./game.getkey.log");
```
Finally, an existing IRD file can be read to create an IRD:
```cs
IRD ird = IRD.Read("./game.ird")
```
An IRD can be then be tweaked, its fields printed, and written to a new IRD:
```cs
ird.UID = 0x9F1A51D8;
ird.Print();
ird.Write("game2.ird");
```
+122 -101
View File
@@ -1,4 +1,5 @@
using System;
using DiscUtils.Iso9660;
using System;
using System.IO;
using System.IO.Hashing;
@@ -51,15 +52,6 @@ namespace LibIRD
/// </summary>
public class ReIRD : IRD
{
#region Properties
/// <summary>
/// ISO file size
/// </summary>
public long Size { get; private set; }
#endregion
#region Constructors
/// <summary>
@@ -67,28 +59,11 @@ namespace LibIRD
/// </summary>
/// <param name="isoPath">Path to the ISO</param>
/// <param name="getKeyLog">Path to the GetKey log file</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="FileNotFoundException"></exception>
/// <exception cref="InvalidDataException"></exception>
public ReIRD(string isoPath, string getKeyLog) : base(isoPath, getKeyLog)
public ReIRD(string isoPath, string getKeyLog) : base(isoPath, getKeyLog, true)
{
// Generate Unique Identifier using ISO CRC32
UID = GenerateUID(isoPath);
// Determine ISO file size
Size = CalculateSize(isoPath);
// Generate Data 2 using Disc ID
DiscID = GenerateID(Size);
// Check that GetKey log matches expected Disc ID
//if (!((ReadOnlySpan<byte>)Data2Key).SequenceEqual(d2))
// throw new InvalidDataException("Unexpected Disc ID in .getkey.log");
// Generate Disc PIC
byte[] pic = GeneratePIC(Size);
// Check that GetKey log matches expected PIC
if (!((ReadOnlySpan<byte>)PIC).SequenceEqual(pic))
throw new InvalidDataException("Unexpected PIC in .getkey.log");
}
/// <summary>
@@ -96,28 +71,27 @@ namespace LibIRD
/// </summary>
/// <param name="isoPath">Path to the ISO</param>
/// <param name="key">Disc Key, redump-style (AES encrypted Data 1)</param>
/// <param name="layerbreak">Layerbreak value, in sectors</param>
/// <param name="region">Disc Region</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="FileNotFoundException"></exception>
public ReIRD(string isoPath, byte[] key, Region region = Region.NONE)
public ReIRD(string isoPath, byte[] key, long? layerbreak = null, Region region = Region.NONE) : base()
{
// Generate Unique Identifier using ISO CRC32
UID = GenerateUID(isoPath);
// Determine ISO file size
Size = CalculateSize(isoPath);
long size = CalculateSize(isoPath);
// Set Disc Key
DiscKey = key;
// Generate Data 2 using Disc ID
DiscID = GenerateID(Size, region);
DiscID = GenerateID(size, region);
// Generate Disc PIC
PIC = GeneratePIC(Size);
PIC = GeneratePIC(isoPath, size, layerbreak * SectorSize);
// Generate IRD fields
GenerateIRD(isoPath);
GenerateIRD(isoPath, true);
}
#endregion
@@ -131,12 +105,12 @@ namespace LibIRD
private static byte[] GenerateID(long size, Region region = Region.NONE)
{
if (size > BDLayerSize) // if BD-50, Disc ID is fixed
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
return [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
else // else if BD-25, Disc ID has a byte referring to disc region
{
return new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0x00, 0x02, 0x00, (byte)region, 0x00, 0x00, 0x00, 0x01 };
return [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0x00, 0x02, 0x00, (byte)region, 0x00, 0x00, 0x00, 0x01 ];
}
}
@@ -147,50 +121,87 @@ namespace LibIRD
/// <param name="layerbreak">Layer break value, byte at which disc layers are split across</param>
/// <param name="exactIRD">True to generate a PIC in 3k3y style (0x03 at 115th byte for BD-50 discs)</param>
/// <exception cref="ArgumentException"></exception>
private static byte[] GeneratePIC(long size, long layerbreak = BDLayerSize, bool exactIRD = false)
private static byte[] GeneratePIC(string isoPath, long size, long? layerbreak = null, bool exactIRD = false)
{
// Validate size
if (size == 0 || (size % SectorSize) != 0)
if (size <= 0 || (size % SectorSize) != 0)
throw new ArgumentException("ISO Size in bytes must be a positive integer multiple of 2048", nameof(size));
// Validate layerbreak
if (layerbreak == 0 || (layerbreak != BDLayerSize && layerbreak >= size))
throw new ArgumentException("Layerbreak in bytes must be a positive integer less than the ISO Size", nameof(size));
// TODO: Generate correct PICs for Hybrid PS3 discs (BD-50 with layerbreak value other than 12219392)
// Validate provided layerbreak
if (layerbreak != null)
{
if (layerbreak <= 0 || (layerbreak >= size))
throw new ArgumentException("Layerbreak in bytes must be a positive integer less than the ISO Size", nameof(size));
if (layerbreak >= 2 * BDLayerSize || layerbreak % SectorSize != 0)
throw new ArgumentException("Unexpected layerbreak value", nameof(size));
}
else
{
// If no layerbreak provided, ensure ISO is not BD-Video hybrid
using FileStream fs = new FileStream(isoPath, FileMode.Open, FileAccess.Read) ?? throw new FileNotFoundException(isoPath);
CDReader reader = new(fs, true, true);
if (reader.DirectoryExists("\\BDMV"))
throw new ArgumentException("Layerbreak must be provided for BD-Video hybrid discs");
// Assume disc has default layerbreak
layerbreak = BDLayerSize;
}
// Generate the PIC based on the size and layerbreak of the ISO
byte[] pic;
if (size > BDLayerSize) // if BD-50
{
// num_sectors + layer_sector_end (0x00100000) + sectors_between_layers (0x01358C00 - 0x00CA73FE) - 3
byte[] total_sectors = BitConverter.GetBytes((uint)(size / SectorSize + 8067071));
// Layer 0 start sector = 0x01000000
long l0_start_sector = 1048576;
// Initial portion of PIC (24 bytes)
pic = new byte[]{
// [4098 bytes] [2x 0x00] ["DI"] [v1] [10units] [DI num]
0x10, 0x02, 0x00, 0x00, 0x44, 0x49, 0x01, 0x10, 0x00, 0x00, 0x20, 0x00,
// ["BDR"] [2 layers]
0x42, 0x44, 0x4F, 0x01, 0x21, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc (4 bytes)
total_sectors[3], total_sectors[2], total_sectors[1], total_sectors[0],
// 1st Layer sector start location (4 bytes)
0x00, 0x10, 0x00, 0x00,
// 1st Layer sector end location (4 bytes)
0x00, 0xCA, 0x73, 0xFE,
// 32 bytes of zeros
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Initial portion of PIC again, for 2nd layer
// ["DI"] [v1] [11unit][DI num]
0x44, 0x49, 0x01, 0x11, 0x00, 0x01, 0x20, 0x00,
// ["BDR"] [2 layers]
0x42, 0x44, 0x4F, 0x01, 0x21, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc
total_sectors[3], total_sectors[2], total_sectors[1], total_sectors[0],
// 2nd Layer sector start location
0x01, 0x35, 0x8C, 0x00,
// 2nd Layer sector end location
0x01, 0xEF, 0xFF, 0xFE,
// Remaining 32 bytes are zeroes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// Layer 0 end sector = start sector + layerbreak - 2
long l0_end_sector = ((long)layerbreak / SectorSize) + l0_start_sector - 2;
// Convert end sector location to hex values for PIC
byte[] l0es = [(byte)((l0_end_sector >> 24) & 0xFF),
(byte)((l0_end_sector >> 16) & 0xFF),
(byte)((l0_end_sector >> 8) & 0xFF),
(byte)((l0_end_sector >> 0) & 0xFF)];
// Layer 1 start sector = end of disc (0x01EFFFFE) - layerbreak + 2
long l1_start_sector = 32505854 - ((long)layerbreak! / SectorSize) + 2;
// Convert start of start sector location to hex values for PIC
byte[] l1ss = [(byte)((l1_start_sector >> 24) & 0xFF),
(byte)((l1_start_sector >> 16) & 0xFF),
(byte)((l1_start_sector >> 8) & 0xFF),
(byte)((l1_start_sector >> 0) & 0xFF)];
// Total sectors used = num_sectors + Layer 0 start + sectors_between_layers (usually 0x01358C00 - 0x00CA73FE - 3)
long total_sectors = (size / SectorSize) + l0_start_sector + (l1_start_sector - l0_end_sector - 3);
byte[] ts = BitConverter.GetBytes((uint)total_sectors);
// Define the PIC
pic = [
// Initial portion of PIC (24 bytes)
// [4098 bytes] [2x 0x00] ["DI"] [v1] [10units] [DI num]
0x10, 0x02, 0x00, 0x00, 0x44, 0x49, 0x01, 0x10, 0x00, 0x00, 0x20, 0x00,
// ["BDR"] [2 layers]
0x42, 0x44, 0x4F, 0x01, 0x21, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc (4 bytes)
ts[3], ts[2], ts[1], ts[0],
// 1st Layer sector start location (4 bytes)
0x00, 0x10, 0x00, 0x00,
// 1st Layer sector end location (4 bytes), 0x00CA73FE for default BD layerbreak of 12219392
l0es[0], l0es[1], l0es[2], l0es[3],
// 32 bytes of zeros
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Initial portion of PIC again, for 2nd layer
// ["DI"] [v1] [11unit][DI num]
0x44, 0x49, 0x01, 0x11, 0x00, 0x01, 0x20, 0x00,
// ["BDR"] [2 layers]
0x42, 0x44, 0x4F, 0x01, 0x21, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc
ts[3], ts[2], ts[1], ts[0],
// 2nd Layer sector start location, 0x01358C00 for default BD layerbreak of 12219392
l1ss[0], l1ss[1], l1ss[2], l1ss[3],
// 2nd Layer sector end location
0x01, 0xEF, 0xFF, 0xFE,
// Remaining 32 bytes are zeroes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ];
// 3k3y style: 0x03 at last byte
if (exactIRD)
pic[114] = 0x03;
@@ -203,23 +214,25 @@ namespace LibIRD
// Layer sector end location: num_sectors + layer_sector_end (0x00100000) - 2
byte[] end_sector = BitConverter.GetBytes((uint)(size / SectorSize + 1048574));
// Initial portion of PIC (24 bytes)
pic = new byte[]{ 0x10, 0x02, 0x00, 0x00, 0x44, 0x49, 0x01, 0x08, 0x00, 0x00, 0x20, 0x00,
0x42, 0x44, 0x4F, 0x01, 0x11, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc (4 bytes)
total_sectors[3], total_sectors[2], total_sectors[1], total_sectors[0],
// Layer sector start location (4 bytes)
0x00, 0x10, 0x00, 0x00,
// Layer sector end location (4 bytes)
end_sector[3], end_sector[2], end_sector[1], end_sector[0],
// Remaining 79 bytes are zeroes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// Define the PIC
pic = [
// Initial portion of PIC (24 bytes)
0x10, 0x02, 0x00, 0x00, 0x44, 0x49, 0x01, 0x08, 0x00, 0x00, 0x20, 0x00,
0x42, 0x44, 0x4F, 0x01, 0x11, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc (4 bytes)
total_sectors[3], total_sectors[2], total_sectors[1], total_sectors[0],
// Layer sector start location (4 bytes)
0x00, 0x10, 0x00, 0x00,
// Layer sector end location (4 bytes)
end_sector[3], end_sector[2], end_sector[1], end_sector[0],
// Remaining 79 bytes are zeroes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ];
}
return pic;
}
@@ -231,12 +244,16 @@ namespace LibIRD
/// <exception cref="FileNotFoundException"></exception>
private static uint GenerateUID(string isoPath)
{
// Validate ISO path
if (isoPath == null || isoPath.Length <= 0)
throw new ArgumentNullException(nameof(isoPath));
// Check file exists
var iso = new FileInfo(isoPath);
FileInfo iso;
try
{
iso = new FileInfo(isoPath);
}
catch (Exception e)
{
throw new ArgumentException("Invalid ISO Path: " + e.Message);
}
if (!iso.Exists)
throw new FileNotFoundException(nameof(isoPath));
@@ -261,12 +278,16 @@ namespace LibIRD
/// <exception cref="FileNotFoundException"></exception>
private static long CalculateSize(string isoPath)
{
// Validate ISO path
if (isoPath == null || isoPath.Length <= 0)
throw new ArgumentNullException(nameof(isoPath));
// Check file exists
var iso = new FileInfo(isoPath);
FileInfo iso;
try
{
iso = new FileInfo(isoPath);
}
catch (Exception e)
{
throw new ArgumentException("Invalid ISO Path: " + e.Message);
}
if (!iso.Exists)
throw new FileNotFoundException(nameof(isoPath));
-25
View File
@@ -1,25 +0,0 @@
using LibIRD;
using System;
using System.IO;
namespace PrintParams
{
internal class Program
{
static void Main()
{
string filename = "./PARAM.SFO";
if (File.Exists(filename))
{
ParamSFO paramSFO = new("./PARAM.SFO");
Console.WriteLine("PARAM.SFO for: " + paramSFO["TITLE_ID"] + '\n');
paramSFO.Print();
}
else
{
Console.WriteLine(filename + " not found");
}
}
}
}
-14
View File
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<Version>0.1</Version>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LibIRD\LibIRD.csproj" />
</ItemGroup>
</Project>
+21 -1
View File
@@ -1 +1,21 @@
# WIP
# Library for ISO Rebuild Data (LibIRD)
.NET library for generating, writing, and reading IRD files. Functionality is provided for deterministically generating IRDs such that a redump ISO and key have a 1-to-1 correspondence with an IRD file.
## What is an IRD?
IRD files contain a summary of what data is on a PlayStation 3 disc. It can be used to rebuild ISOs from files (JB folders). IRD files are also useful for decrypting ISOs after they have been dumped on a PC blu-ray drive, such as for the purposes of legally emulating your PS3 games.
## How to use IRDKit
IRDKit is a tool that allows direct use of LibIRD functionality from a command line interface. The basic usage is:
- Printing info about an ISO: `irdkit info game.iso`
- Printing info about all ISOs and IRDs in a folder: `irdkit info .`
- Creating an IRD from an ISO: `irdkit create game.iso`
- Finding differences between two IRDs: `irdkit diff game1.ird game2.ird`
For detailed usage, read more [here](IRDKit).
## Using the LibIRD library
LibIRD was originally made for creating reproducible, redump-style IRDs when dumping PS3 discs with [MPF](https://github.com/SabreTools/MPF). If you wish to integrate LibIRD into your own application, read the examples [here](LibIRD).
+15
View File
@@ -0,0 +1,15 @@
dotnet publish -c Release -f net8.0 -r win-x86 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r win-x64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r win-arm64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r linux-x64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r linux-arm64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r osx-x64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r osx-arm64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/win-x86/publish/irdkit.exe" -Destination "./irdkit-win-x86.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/win-x64/publish/irdkit.exe" -Destination "./irdkit-win-x64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/win-arm64/publish/irdkit.exe" -Destination "./irdkit-win-arm64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/linux-x64/publish/irdkit" -Destination "./irdkit-linux-x64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/linux-arm64/publish/irdkit" -Destination "./irdkit-linux-arm64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/osx-x64/publish/irdkit" -Destination "./irdkit-osx-x64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/osx-arm64/publish/irdkit" -Destination "./irdkit-osx-arm64.zip" -CompressionLevel "Optimal"