10 Commits
Author SHA1 Message Date
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
15 changed files with 951 additions and 389 deletions
-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>
+38
View File
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<OutputType>Exe</OutputType>
<PackAsTool>true</PackAsTool>
<ToolCommandName>irdkit</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
<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.2.0</Version>
<!-- Package Properties -->
<Authors>Deterous</Authors>
<Description>Library for ISO Rebuild Data</Description>
<Copyright>Copyright (c) Deterous 2023</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" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="DiscUtils.Core" Version="0.16.13" />
<PackageReference Include="DiscUtils.Iso9660" Version="0.16.13" />
<PackageReference Include="SabreTools.RedumpLib" Version="1.3.1" />
</ItemGroup>
</Project>
+323
View File
@@ -0,0 +1,323 @@
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.Hashing;
using System.Security.Cryptography;
using System.Text;
namespace IRDKit
{
internal class Program
{
/// <summary>
/// IRD Creation Verb
/// </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 string ISOPath { get; set; }
[Value(1, Required = false, HelpText = "Path to the IRD file to be created")]
public string IRDPath { get; set; }
[Option('b', "layerbreak", HelpText = "Layerbreak value in bytes (define for 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; }
}
/// <summary>
/// IRD or ISO information verb
/// </summary>
[Verb("info", HelpText = "Print information from an IRD or ISO")]
public class InfoOptions
{
[Value(0, Required = true, HelpText = "Path to the IRD or ISO file to be printed")]
public string InPath { get; set; }
[Value(1, Required = false, HelpText = "Path to the text or json file to be created")]
public string OutPath { get; set; }
[Option('j', "json", HelpText = "Print IRD or ISO information as a JSON object")]
public bool Json { get; set; }
}
/// <summary>
/// Parse command line arguments
/// </summary>
/// <param name="args">Command line arguments</param>
public static void Main(string[] args)
{
var result = Parser.Default.ParseArguments<CreateOptions, InfoOptions>(args).WithParsed(Run);
}
/// <summary>
/// Perform
/// </summary>
/// <param name="obj"></param>
/// <exception cref="FileNotFoundException"></exception>
/// <exception cref="InvalidFileSystemException"></exception>
private static void Run(object obj)
{
switch (obj)
{
case CreateOptions opt:
Console.OutputEncoding = Encoding.UTF8;
// Validate ISO path
ArgumentNullException.ThrowIfNull(opt.ISOPath);
// If directory, search for all ISOs in current directory
if (Directory.Exists(opt.ISOPath))
{
// If recurse option enabled, search recursively
IEnumerable<string> isoFiles;
if (opt.Recurse)
{
Console.WriteLine($"Recursively searching for ISOs in {opt.ISOPath}");
isoFiles = Directory.EnumerateFiles(opt.ISOPath, "*.iso", SearchOption.AllDirectories);
}
else
{
Console.WriteLine($"Searching for ISOs in {opt.ISOPath}");
isoFiles = Directory.EnumerateFiles(opt.ISOPath, "*.iso", SearchOption.TopDirectoryOnly);
}
// Create an IRD file for all ISO files found
foreach (string file in isoFiles)
ProcessISO(opt, file);
break;
}
// Create a single IRD from an ISO
if (File.Exists(opt.ISOPath))
{
ProcessISO(opt, opt.ISOPath, opt.IRDPath);
break;
}
throw new ArgumentException("Not a valid ISO file or directory");
case InfoOptions opt:
string filetype = Path.GetExtension(opt.InPath);
if (String.Compare(filetype, ".iso", StringComparison.OrdinalIgnoreCase) == 0)
{
// Open ISO file for reading
using FileStream fs = new FileStream(opt.InPath, FileMode.Open, FileAccess.Read) ?? throw new FileNotFoundException(opt.InPath);
// Validate ISO file stream
if (!CDReader.Detect(fs))
throw new InvalidFileSystemException("Not a valid ISO file");
// Create new ISO reader
CDReader reader = new(fs, true, true);
File.WriteAllText(opt.OutPath, "{\n");
// Write PS3_DISC.SFB info
using (DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_DISC.SFB", FileMode.Open, FileAccess.Read))
{
try
{
PS3_DiscSFB ps3_DiscSFB = new(s);
if (opt.Json)
{
File.AppendAllText(opt.OutPath, "\"PS3_DISC.SFB\": ");
ps3_DiscSFB.PrintJson(opt.OutPath);
File.AppendAllText(opt.OutPath, ",");
}
else
ps3_DiscSFB.Print(opt.OutPath);
}
catch
{
Console.WriteLine("PS3_DISC.SFB not found");
}
}
// Write PARAM.SFO info
using (DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read))
{
try
{
ParamSFO paramSFO = new(s);
if (opt.Json)
{
File.AppendAllText(opt.OutPath, "\n\"PARAM.SFO\": ");
paramSFO.PrintJson(opt.OutPath);
}
else
paramSFO.Print(opt.OutPath);
}
catch
{
Console.WriteLine("PS3_GAME\\PARAM.SFO not found");
}
}
File.AppendAllText(opt.OutPath, "\n}");
}
else
{
// Assume it is an IRD file
if (opt.Json)
IRD.Read(opt.InPath).PrintJson(opt.OutPath);
else
IRD.Read(opt.InPath).Print(opt.OutPath);
}
break;
}
}
public static void ProcessISO(CreateOptions opt, string isoPath, string irdPath = null)
{
// Check file exists
var iso = new FileInfo(isoPath);
if (!iso.Exists)
{
Console.WriteLine($"{nameof(isoPath)} is not a valid File or Directory");
return;
}
Console.WriteLine($"Reading {isoPath}");
// Create new reproducible redump-style IRD with a given hex key
if (opt.Key != null)
{
try
{
// Get disc key from hex string
byte[] discKey = Convert.FromHexString(opt.Key);
Console.WriteLine($"Creating reproducible, redump-style IRD with Key: {opt.Key}");
IRD ird1 = new ReIRD(isoPath, discKey, opt.Layerbreak);
ird1.Write(irdPath ?? Path.GetFileNameWithoutExtension(isoPath) + ".ird");
ird1.Print();
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found");
}
return;
}
// Create new reproducible redump-style IRD with a given key file
if (opt.KeyFile != null)
{
try
{
// Read key from .key file
byte[] discKey = File.ReadAllBytes(opt.KeyFile);
Console.WriteLine($"Creating reproducible, redump-style IRD with Key: {Convert.ToHexString(discKey)}");
IRD ird2 = new ReIRD(isoPath, discKey, opt.Layerbreak);
ird2.Write(irdPath ?? Path.GetFileNameWithoutExtension(isoPath) + ".ird");
ird2.Print();
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found");
}
return;
}
// Create new reproducible redump-style IRD with a given GetKey log
if (opt.GetKeyLog != null)
{
try
{
Console.WriteLine($"Creating reproducible, redump-style IRD with key from: {opt.GetKeyLog}");
IRD ird3 = new ReIRD(isoPath, opt.GetKeyLog);
ird3.Write(irdPath ?? Path.GetFileNameWithoutExtension(isoPath) + ".ird");
ird3.Print();
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found");
}
return;
}
// No key provided, try get key from redump.org
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.WriteLine("ISO not found in redump, cannot automatically retreive key.");
return;
}
else if (ids.Count > 1)
{
// Compute SHA1 hash
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.WriteLine("ISO not found in redump, cannot automatically retreive key.");
return;
}
else if (ids2.Count > 1)
{
Console.WriteLine("Cannot automatically get key from redump. Please search redump.org and run again with -k");
return;
}
id = ids2[0];
}
else
{
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.WriteLine("Invalid key obtained from redump");
}
// Create IRD with key from redump
Console.WriteLine($"Creating reproducible, redump-style IRD with Key: {Convert.ToHexString(key)}");
IRD ird = new ReIRD(isoPath, key, opt.Layerbreak);
ird.Write(irdPath ?? Path.GetFileNameWithoutExtension(isoPath) + ".ird");
ird.Print();
}
}
}
+7
View File
@@ -0,0 +1,7 @@
## How to use IRDKit
IRDKit is a tool that allows direct use of LibIRD functionality from the command line interface. The basic usage is:
```
irdkit game.iso game.ird
```
For detailed usage, run `irdkit --help`
+5 -11
View File
@@ -5,9 +5,7 @@ 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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintParams", "PrintParams\PrintParams.csproj", "{B5D9C0AD-EA8D-486E-A1D7-ED2C3C7163BC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRDKit", "IRDKit\IRDKit.csproj", "{9060E3AF-E4FB-436F-93A1-5C47389CB88E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -19,14 +17,10 @@ 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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+157 -43
View File
@@ -6,6 +6,7 @@ using System.IO.Compression;
using System.IO.Hashing;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace LibIRD
{
@@ -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>
@@ -217,7 +218,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 +313,7 @@ namespace LibIRD
string titleID,
string title,
string sysVersion,
string gameVersion,
string discVersion,
string appVersion,
byte[] header,
byte[] footer,
@@ -329,7 +330,7 @@ namespace LibIRD
TitleID = titleID;
Title = title;
SystemVersion = sysVersion;
GameVersion = gameVersion;
DiscVersion = discVersion;
AppVersion = appVersion;
HeaderLength = (uint)header.Length;
Header = header;
@@ -364,7 +365,8 @@ 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</param>
public IRD(string isoPath, byte[] discKey, byte[] discID, byte[] discPIC, bool redump = false)
{
// Parse ISO, Disc Key, Disc ID, and PIC
DiscKey = discKey;
@@ -373,7 +375,7 @@ namespace LibIRD
PIC = discPIC;
// Generate IRD files from ISO
GenerateIRD(isoPath);
GenerateIRD(isoPath, redump);
}
/// <summary>
@@ -381,15 +383,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 +407,7 @@ namespace LibIRD
private protected static byte[] GenerateD1(byte[] key)
{
// Validate key
if (key == null)
throw new ArgumentNullException(nameof(key));
ArgumentNullException.ThrowIfNull(key, nameof(key));
if (key.Length != 16)
throw new ArgumentException("Disc Key must be a byte array of length 16", nameof(key));
@@ -441,8 +441,7 @@ namespace LibIRD
private protected static byte[] GenerateDiscKey(byte[] d1)
{
// Validate key
if (d1 == null)
throw new ArgumentNullException(nameof(d1));
ArgumentNullException.ThrowIfNull(d1, nameof(d1));
if (d1.Length != 16)
throw new ArgumentException("Disc Key must be a byte array of length 16", nameof(d1));
@@ -476,10 +475,8 @@ 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));
ArgumentNullException.ThrowIfNull(d2, 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 +508,7 @@ namespace LibIRD
private protected static byte[] GenerateDiscID(byte[] d2)
{
// Validate id
if (d2 == null)
throw new ArgumentNullException(nameof(d2));
ArgumentNullException.ThrowIfNull(d2 , nameof(d2));
if (d2.Length != 16)
throw new ArgumentException("Disc ID must be a byte array of length 16", nameof(d2));
@@ -547,8 +543,7 @@ namespace LibIRD
{
// Validate .getkey.log file path
if (getKeyLog == null)
throw new ArgumentNullException(nameof(getKeyLog));
ArgumentNullException.ThrowIfNull(getKeyLog, nameof(getKeyLog));
if (!File.Exists(getKeyLog))
throw new FileNotFoundException(nameof(getKeyLog));
@@ -624,10 +619,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 +634,41 @@ 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 DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_DISC.SFB", FileMode.Open, FileAccess.Read);
// Parse PS3_DISC.SFB file
PS3_DiscSFB ps3_DiscSFB = new(s);
bool title_id_found = ps3_DiscSFB.Field.TryGetValue("TITLE_ID", out string title_id);
// If a valid TITLE_ID field is present, remove the hyphen to fit into standard IRD file
if (title_id_found && title_id.Length == 10 && title_id[4] == '-')
TitleID = string.Concat(title_id.AsSpan(0, 4), title_id.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 version_found = ps3_DiscSFB.Field.TryGetValue("VERSION", out string disc_version);
if (version_found)
DiscVersion = disc_version;
}
// Read PS3 Metadata from PARAM.SFO
using (DiscUtils.Streams.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
TitleID ??= paramSFO.Field["TITLE_ID"];
Title = paramSFO.Field["TITLE"];
// If PS3_DISC.SFB did not set DiscVersion, use PARAM.SFO VERSION
DiscVersion ??= paramSFO.Field["VERSION"];
AppVersion = paramSFO.Field["APP_VER"];
}
// Determine system update version
@@ -674,7 +695,7 @@ 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: ");
Array.Sort(FileKeys, FileHashes);
@@ -891,12 +912,13 @@ 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)
@@ -960,7 +982,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 +1071,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);
@@ -1166,7 +1188,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)
@@ -1227,7 +1249,7 @@ namespace LibIRD
titleID,
title,
sysVersion,
gameVersion,
discVersion,
appVersion,
header,
footer,
@@ -1242,6 +1264,98 @@ 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)
{
// Build string from parameters
StringBuilder printText = new();
printText.AppendLine("IRD Contents:");
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.WriteAllText(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)
{
// 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)}\"");
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
}
}
+13 -6
View File
@@ -2,25 +2,32 @@
<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.2.0</Version>
<!-- 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</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>
</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" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
</ItemGroup>
</Project>
+160
View File
@@ -0,0 +1,160 @@
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>
/// <exception cref="ArgumentNullException"></exception>
public PS3_DiscSFB(string sfbPath)
{
// Validate file path
ArgumentNullException.ThrowIfNull(sfbPath, nameof(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)
{
// Build string from parameters
StringBuilder printText = new();
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>
/// 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, new JsonSerializerOptions { WriteIndented = true });
// 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);
}
}
}
}
+86 -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,118 @@ 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)
{
// Build string from parameters
StringBuilder print = new("PARAM.SFO Contents:\n====================\n");
StringBuilder printText = new();
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>
/// 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, new JsonSerializerOptions { WriteIndented = true });
// 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");
```
+99 -75
View File
@@ -56,7 +56,7 @@ namespace LibIRD
/// <summary>
/// ISO file size
/// </summary>
public long Size { get; private set; }
private long Size { get; set; }
#endregion
@@ -67,10 +67,9 @@ 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>
/// <param name="layerbreak">Layerbreak value, in sectors</param>
/// <exception cref="InvalidDataException"></exception>
public ReIRD(string isoPath, string getKeyLog) : base(isoPath, getKeyLog)
public ReIRD(string isoPath, string getKeyLog, long? layerbreak = null) : base(isoPath, getKeyLog, true)
{
// Generate Unique Identifier using ISO CRC32
UID = GenerateUID(isoPath);
@@ -80,12 +79,10 @@ namespace LibIRD
// 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);
byte[] pic = GeneratePIC(Size, layerbreak * SectorSize);
// Check that GetKey log matches expected PIC
if (!((ReadOnlySpan<byte>)PIC).SequenceEqual(pic))
throw new InvalidDataException("Unexpected PIC in .getkey.log");
@@ -96,10 +93,9 @@ 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)
{
// Generate Unique Identifier using ISO CRC32
UID = GenerateUID(isoPath);
@@ -114,10 +110,10 @@ namespace LibIRD
DiscID = GenerateID(Size, region);
// Generate Disc PIC
PIC = GeneratePIC(Size);
PIC = GeneratePIC(Size, layerbreak * SectorSize);
// Generate IRD fields
GenerateIRD(isoPath);
GenerateIRD(isoPath, true);
}
#endregion
@@ -131,12 +127,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,55 +143,83 @@ 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(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));
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));
}
// If layerbreak value was not set, assume it is a non-hybrid disc with default layerbreak
long layer_break = layerbreak ?? BDLayerSize;
// TODO: Generate correct PICs for Hybrid PS3 discs (BD-50 with layerbreak value other than 12219392)
// 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;
// Layer 0 end sector = start sector + layerbreak - 2
long l0_end_sector = (layer_break / 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)];
// 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 };
// 3k3y style: 0x03 at last byte
if (exactIRD)
pic[114] = 0x03;
// Layer 1 start sector = end of disc (0x01EFFFFE) - layerbreak + 2
long l1_start_sector = 32505854 - (layer_break / 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;
}
else // if BD-25
{
// Total sectors used on disc: num_sectors + layer_sector_end (0x00100000) - 1
@@ -203,21 +227,23 @@ 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;
@@ -232,8 +258,7 @@ namespace LibIRD
private static uint GenerateUID(string isoPath)
{
// Validate ISO path
if (isoPath == null || isoPath.Length <= 0)
throw new ArgumentNullException(nameof(isoPath));
ArgumentNullException.ThrowIfNull(isoPath, nameof(isoPath));
// Check file exists
var iso = new FileInfo(isoPath);
@@ -262,8 +287,7 @@ namespace LibIRD
private static long CalculateSize(string isoPath)
{
// Validate ISO path
if (isoPath == null || isoPath.Length <= 0)
throw new ArgumentNullException(nameof(isoPath));
ArgumentNullException.ThrowIfNull(isoPath, nameof(isoPath));
// Check file exists
var iso = new FileInfo(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>
+23 -1
View File
@@ -1 +1,23 @@
# 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 the command line interface. The basic usage is:
```
irdkit create game.iso
```
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).
## Limitations
Currently, LibIRD does not produce correct IRDs for ISOs with non-contiguous files. Therefore, do not consider this library "stable" until v1.0.0 is released.