19 Commits
Author SHA1 Message Date
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
11 changed files with 799 additions and 246 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "DiscUtils"]
path = DiscUtils
url = https://github.com/Deterous/DiscUtils
Submodule
+1
Submodule DiscUtils added at f53a1dab61
+4 -4
View File
@@ -10,7 +10,7 @@
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>0.2.0</Version> <Version>0.3.0</Version>
<!-- Package Properties --> <!-- Package Properties -->
<Authors>Deterous</Authors> <Authors>Deterous</Authors>
@@ -29,10 +29,10 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LibIRD\LibIRD.csproj" /> <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="CommandLineParser" Version="2.9.1" />
<PackageReference Include="DiscUtils.Core" Version="0.16.13" /> <PackageReference Include="SabreTools.RedumpLib" Version="1.3.2" />
<PackageReference Include="DiscUtils.Iso9660" Version="0.16.13" />
<PackageReference Include="SabreTools.RedumpLib" Version="1.3.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+613 -128
View File
@@ -6,7 +6,9 @@ using SabreTools.RedumpLib.Web;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.IO.Hashing; using System.IO.Hashing;
using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@@ -14,19 +16,21 @@ namespace IRDKit
{ {
internal class Program internal class Program
{ {
#region Options
/// <summary> /// <summary>
/// IRD Creation Verb /// IRD Creation command
/// </summary> /// </summary>
[Verb("create", HelpText = "Create an IRD from an ISO")] [Verb("create", HelpText = "Create an IRD from an ISO")]
public class CreateOptions public class CreateOptions
{ {
[Value(0, Required = true, HelpText = "Path to an ISO file, or directory of ISO files")] [Value(0, Required = true, HelpText = "Path to an ISO file, or directory of ISO files")]
public string ISOPath { get; set; } public IEnumerable<string> ISOPath { get; set; }
[Value(1, Required = false, HelpText = "Path to the IRD file to be created")] [Option('o', "output", HelpText = "Path to the IRD file to be created (will overwrite)")]
public string IRDPath { get; set; } public string IRDPath { get; set; }
[Option('b', "layerbreak", HelpText = "Layerbreak value in bytes (define for BD-Video hybrid discs). Default: 12219392")] [Option('b', "layerbreak", HelpText = "Layerbreak value in bytes (use with BD-Video hybrid discs). Default: 12219392")]
public long? Layerbreak { get; set; } public long? Layerbreak { get; set; }
[Option('k', "key", HelpText = "Hexadecimal representation of the disc key")] [Option('k', "key", HelpText = "Hexadecimal representation of the disc key")]
@@ -43,217 +47,677 @@ namespace IRDKit
} }
/// <summary> /// <summary>
/// IRD or ISO information verb /// IRD or ISO information command
/// </summary> /// </summary>
[Verb("info", HelpText = "Print information from an IRD or ISO")] [Verb("info", HelpText = "Print information from an IRD or ISO")]
public class InfoOptions public class InfoOptions
{ {
[Value(0, Required = true, HelpText = "Path to the IRD or ISO file to be printed")] [Value(0, Required = true, HelpText = "Path to an IRD or ISO file, or directory of IRD and/or ISO files")]
public string InPath { get; set; } public IEnumerable<string> InPath { get; set; }
[Value(1, Required = false, HelpText = "Path to the text or json file to be created")] [Option('o', "output", HelpText = "Path to the text or json file to be created (will overwrite)")]
public string OutPath { get; set; } public string OutPath { get; set; }
[Option('j', "json", HelpText = "Print IRD or ISO information as a JSON object")] [Option('j', "json", HelpText = "Print IRD or ISO information as a JSON object")]
public bool Json { get; set; } 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> /// <summary>
/// Parse command line arguments /// Parse command line arguments
/// </summary> /// </summary>
/// <param name="args">Command line arguments</param> /// <param name="args">Command line arguments</param>
public static void Main(string[] args) public static void Main(string[] args)
{ {
var result = Parser.Default.ParseArguments<CreateOptions, InfoOptions>(args).WithParsed(Run); // Ensure console prints foreign characters properly
Console.OutputEncoding = Encoding.UTF8;
// Parse arguments
var result = Parser.Default.ParseArguments<CreateOptions, InfoOptions, DiffOptions>(args).WithParsed(Run);
} }
/// <summary> /// <summary>
/// Perform /// Parse arguments
/// </summary> /// </summary>
/// <param name="obj"></param> /// <param name="args">Command-line arguments</param>
/// <exception cref="FileNotFoundException"></exception> /// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidFileSystemException"></exception> private static void Run(object args)
private static void Run(object obj)
{ {
switch (obj) switch (args)
{ {
// Process options from a `create` command
case CreateOptions opt: case CreateOptions opt:
Console.OutputEncoding = Encoding.UTF8;
// Validate ISO path // Validate ISO paths
ArgumentNullException.ThrowIfNull(opt.ISOPath); ArgumentNullException.ThrowIfNull(opt.ISOPath);
// If directory, search for all ISOs in current directory foreach (string isoPath in opt.ISOPath)
if (Directory.Exists(opt.ISOPath))
{ {
// If recurse option enabled, search recursively // Validate ISO path
IEnumerable<string> isoFiles; ArgumentNullException.ThrowIfNull(isoPath);
if (opt.Recurse)
// If directory, search for all ISOs in current directory
if (Directory.Exists(isoPath))
{ {
Console.WriteLine($"Recursively searching for ISOs in {opt.ISOPath}"); // If recurse option enabled, search recursively
isoFiles = Directory.EnumerateFiles(opt.ISOPath, "*.iso", SearchOption.AllDirectories); IEnumerable<string> isoFiles;
if (opt.Recurse)
{
if (isoPath == ".")
Console.WriteLine($"Recursively searching for ISOs in current directory");
else
Console.WriteLine($"Recursively searching for ISOs in {isoPath}");
isoFiles = Directory.EnumerateFiles(isoPath, "*.iso", SearchOption.AllDirectories);
}
else
{
if (isoPath == ".")
Console.WriteLine($"Searching for ISOs in current directory");
else
Console.WriteLine($"Searching for ISOs in {isoPath}");
isoFiles = Directory.EnumerateFiles(isoPath, "*.iso", SearchOption.TopDirectoryOnly);
}
// Warn if no files are found
if (!isoFiles.Any())
Console.WriteLine("No ISOs found (ensure .iso extension)");
// Create an IRD file for all ISO files found
foreach (string file in isoFiles)
ISO2IRD(file);
} }
else else
{ {
Console.WriteLine($"Searching for ISOs in {opt.ISOPath}"); // Check that given file exists
isoFiles = Directory.EnumerateFiles(opt.ISOPath, "*.iso", SearchOption.TopDirectoryOnly); if (!File.Exists(isoPath)) throw new ArgumentException("Not a valid file or directory");
// Save to given output path, if only 1 IRD is being created
if (opt.ISOPath.Count() == 1 && opt.IRDPath != null && opt.IRDPath != "")
{
string irdPath = ISO2IRD(isoPath, opt.IRDPath, opt.Key, opt.KeyFile, opt.GetKeyLog, opt.Layerbreak);
if (irdPath != null)
Console.WriteLine($"IRD saved to {irdPath}");
}
else
{
string irdPath = ISO2IRD(isoPath, null, opt.Key, opt.KeyFile, opt.GetKeyLog, opt.Layerbreak);
if (irdPath != null)
Console.WriteLine($"IRD saved to {irdPath}");
}
} }
// 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 break;
if (File.Exists(opt.ISOPath))
{
ProcessISO(opt, opt.ISOPath, opt.IRDPath);
break;
}
throw new ArgumentException("Not a valid ISO file or directory");
// Process options from an `info` command
case InfoOptions opt: case InfoOptions opt:
string filetype = Path.GetExtension(opt.InPath);
if (String.Compare(filetype, ".iso", StringComparison.OrdinalIgnoreCase) == 0) // Validate required parameter
ArgumentNullException.ThrowIfNull(opt.InPath);
// Clear the output file path if it exists
if (opt.OutPath != null && opt.OutPath != "")
File.Delete(opt.OutPath);
foreach (string filePath in opt.InPath)
{ {
// Open ISO file for reading // Validate path
using FileStream fs = new FileStream(opt.InPath, FileMode.Open, FileAccess.Read) ?? throw new FileNotFoundException(opt.InPath); ArgumentNullException.ThrowIfNull(filePath);
// 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"); // If directory, search for all ISOs in current directory
if (Directory.Exists(filePath))
// Write PS3_DISC.SFB info
using (DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_DISC.SFB", FileMode.Open, FileAccess.Read))
{ {
try // If recurse option enabled, search recursively
IEnumerable<string> irdFiles;
IEnumerable<string> isoFiles;
if (opt.Recurse)
{ {
PS3_DiscSFB ps3_DiscSFB = new(s); if (filePath == ".")
if (opt.Json) Console.WriteLine($"Recursively searching for IRDs and ISOs in current directory...\n");
{
File.AppendAllText(opt.OutPath, "\"PS3_DISC.SFB\": ");
ps3_DiscSFB.PrintJson(opt.OutPath);
File.AppendAllText(opt.OutPath, ",");
}
else else
ps3_DiscSFB.Print(opt.OutPath); 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);
} }
catch else
{ {
Console.WriteLine("PS3_DISC.SFB not found"); if (filePath == ".")
} Console.WriteLine($"Searching for IRDs and ISOs in current directory...\n");
}
// 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 else
paramSFO.Print(opt.OutPath); Console.WriteLine($"Searching for IRDs and ISOs in {filePath}...\n");
irdFiles = Directory.EnumerateFiles(filePath, "*.ird", SearchOption.TopDirectoryOnly);
isoFiles = Directory.EnumerateFiles(filePath, "*.iso", SearchOption.TopDirectoryOnly);
} }
catch
{
Console.WriteLine("PS3_GAME\\PARAM.SFO not found");
}
}
File.AppendAllText(opt.OutPath, "\n}"); // Warn if no files are found
} if (!isoFiles.Any() && !irdFiles.Any())
else Console.WriteLine("No IRDs or ISOs found (ensure .ird and .iso extensions)");
{
// Assume it is an IRD file // Open JSON object
if (opt.Json) if (opt.Json)
IRD.Read(opt.InPath).PrintJson(opt.OutPath); {
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.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 else
IRD.Read(opt.InPath).Print(opt.OutPath); {
// Check that given file exists
if (!File.Exists(filePath)) throw new ArgumentException($"{filePath} is not a valid file or directory");
// 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; break;
// Process options from a `diff` command
case DiffOptions opt:
// Validate required parameter
ArgumentNullException.ThrowIfNull(opt.InPath1);
ArgumentNullException.ThrowIfNull(opt.InPath2);
if (!File.Exists(opt.InPath1)) throw new ArgumentException($"{opt.InPath1} is not a valid file or directory");
if (!File.Exists(opt.InPath2)) throw new ArgumentException($"{opt.InPath2} is not a valid file or directory");
// 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;
} }
} }
public static void ProcessISO(CreateOptions opt, string isoPath, string irdPath = null) #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 file exists // Check if file is an ISO
var iso = new FileInfo(isoPath); bool isISO = String.Compare(Path.GetExtension(inPath), ".iso", StringComparison.OrdinalIgnoreCase) == 0;
if (!iso.Exists) if (isISO)
{ {
Console.WriteLine($"{nameof(isoPath)} is not a valid File or Directory"); 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; return;
} }
Console.WriteLine($"Reading {isoPath}"); catch (InvalidDataException)
{
// Not a valid IRD file despite extension, give up
if (json)
return;
if (isISO)
Console.WriteLine($"{inPath} is not a valid ISO file\n");
else
Console.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 FileStream(isoPath, FileMode.Open, FileAccess.Read) ?? throw new FileNotFoundException(isoPath);
// Validate ISO file stream
if (!CDReader.Detect(fs))
throw new InvalidFileSystemException($"{isoPath} is not a valid ISO file");
// 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.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.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.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();
if (IRD1.Version != IRD2.Version)
printText.AppendLine($"Version: {IRD1.Version} vs {IRD2.Version}");
if (IRD1.TitleID != IRD2.TitleID)
printText.AppendLine($"TitleID: {IRD1.TitleID} vs {IRD2.TitleID}");
if (IRD1.Title != IRD2.Title)
printText.AppendLine($"Title: {IRD1.Title} vs {IRD2.Title}");
if (IRD1.SystemVersion != IRD2.SystemVersion)
printText.AppendLine($"PUP Version: {IRD1.SystemVersion} vs {IRD2.SystemVersion}");
if (IRD1.DiscVersion != IRD2.DiscVersion)
printText.AppendLine($"Disc Version: {IRD1.DiscVersion} vs {IRD2.DiscVersion}");
if (IRD1.AppVersion != IRD2.AppVersion)
printText.AppendLine($"App Version: {IRD1.AppVersion} vs {IRD2.AppVersion}");
byte[] header1 = Decompress(IRD1.Header);
byte[] header2 = Decompress(IRD2.Header);
if (header1.Length != header2.Length)
printText.AppendLine($"Header Length: {header1.Length} vs {header2.Length}");
if (!header1.SequenceEqual(header2))
printText.AppendLine($"Header: Differs");
byte[] footer1 = Decompress(IRD1.Footer);
byte[] footer2 = Decompress(IRD2.Footer);
if (footer1.Length != footer2.Length)
printText.AppendLine($"Footer Length: {footer1.Length} vs {footer2.Length}");
if (!footer1.SequenceEqual(footer2))
printText.AppendLine($"Footer: Differs");
if (IRD1.RegionCount != IRD2.RegionCount)
printText.AppendLine($"Region Count: {IRD1.RegionCount} vs {IRD2.RegionCount}");
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])}");
}
if (IRD1.FileCount != IRD2.FileCount)
printText.AppendLine($"File Count: {IRD1.FileCount} vs {IRD2.FileCount}");
int fileCount = IRD2.FileCount < IRD1.FileCount ? (int)IRD2.FileCount : (int)IRD1.FileCount;
if (fileCount > IRD1.FileKeys.Length)
fileCount = IRD1.FileKeys.Length;
if (fileCount > IRD2.FileKeys.Length)
fileCount = IRD2.FileKeys.Length;
if (fileCount > IRD1.FileHashes.Length)
fileCount = IRD1.FileHashes.Length;
if (fileCount > IRD2.FileHashes.Length)
fileCount = IRD2.FileHashes.Length;
for (int i = 0; i < fileCount; i++)
{
if (IRD1.FileKeys[i] != IRD2.FileKeys[i])
printText.AppendLine($"File {i} Offset: {IRD1.FileKeys[i]} vs {IRD2.FileKeys[i]}");
if (!IRD1.FileHashes[i].SequenceEqual(IRD2.FileHashes[i]))
printText.AppendLine($"File {i} Hash: {Convert.ToHexString(IRD1.FileHashes[i])} vs {Convert.ToHexString(IRD2.FileHashes[i])}");
}
if (IRD1.ExtraConfig != IRD2.ExtraConfig)
printText.AppendLine($"Extra Config: {IRD1.ExtraConfig:X4} vs {IRD2.ExtraConfig:X4}");
if (IRD1.Attachments != IRD2.Attachments)
printText.AppendLine($"Attachments: {IRD1.Attachments:X4} vs {IRD2.Attachments:X4}");
if (IRD1.UID != IRD2.UID)
printText.AppendLine($"Unique ID: {IRD1.UID:X8} vs {IRD2.UID:X8}");
if (!IRD1.Data1Key.SequenceEqual(IRD2.Data1Key))
printText.AppendLine($"Data 1 Key: {Convert.ToHexString(IRD1.Data1Key)} vs {Convert.ToHexString(IRD2.Data1Key)}");
if (!IRD1.Data2Key.SequenceEqual(IRD2.Data2Key))
printText.AppendLine($"Data 2 Key: {Convert.ToHexString(IRD1.Data2Key)} vs {Convert.ToHexString(IRD2.Data2Key)}");
if (!IRD1.PIC.SequenceEqual(IRD2.PIC))
printText.AppendLine($"PIC: {Convert.ToHexString(IRD1.PIC)} vs {Convert.ToHexString(IRD2.PIC)}");
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)
{
// Check file exists
FileInfo iso = new(isoPath);
if (!iso.Exists)
{
Console.WriteLine($"{nameof(isoPath)} is not a valid file or directory");
return null;
}
// Determine IRD path if none given
irdPath ??= Path.ChangeExtension(isoPath, ".ird");
// Create new reproducible redump-style IRD with a given hex key // Create new reproducible redump-style IRD with a given hex key
if (opt.Key != null) if (hexKey != null)
{ {
try try
{ {
// Get disc key from hex string // Get disc key from hex string
byte[] discKey = Convert.FromHexString(opt.Key); byte[] discKey = Convert.FromHexString(hexKey);
if (discKey == null || discKey.Length != 16)
throw new ArgumentException(hexKey);
Console.WriteLine($"Creating reproducible, redump-style IRD with Key: {opt.Key}"); Console.WriteLine($"Creating {irdPath} with Key: {hexKey}");
IRD ird1 = new ReIRD(isoPath, discKey, opt.Layerbreak); IRD ird1 = new ReIRD(isoPath, discKey, layerbreak);
ird1.Write(irdPath ?? Path.GetFileNameWithoutExtension(isoPath) + ".ird"); ird1.Write(irdPath);
ird1.Print(); ird1.Print();
return irdPath;
}
catch (ArgumentException)
{
Console.Error.WriteLine($"{hexKey} is not a valid key, detecting key automatically...");
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
Console.Error.WriteLine("File not found"); Console.Error.WriteLine("File not found, failed to create IRD");
return null;
} }
return;
} }
// Create new reproducible redump-style IRD with a given key file // Create new reproducible redump-style IRD with a given key file
if (opt.KeyFile != null) if (keyPath != null)
{ {
// Read key from .key file
byte[] discKey = File.ReadAllBytes(keyPath);
try try
{ {
// Read key from .key file IRD ird2 = new ReIRD(isoPath, discKey, layerbreak);
byte[] discKey = File.ReadAllBytes(opt.KeyFile); Console.WriteLine($"Creating {irdPath} with Key: {Convert.ToHexString(discKey)}");
ird2.Write(irdPath);
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(); ird2.Print();
return irdPath;
}
catch (ArgumentException)
{
Console.Error.WriteLine($"{Convert.ToHexString(discKey)} is not a valid key, detecting key automatically...");
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
Console.Error.WriteLine("File not found"); Console.Error.WriteLine("File not found, failed to create IRD");
return null;
} }
return;
} }
// Create new reproducible redump-style IRD with a given GetKey log // Create new reproducible redump-style IRD with a given GetKey log
if (opt.GetKeyLog != null) if (getKeyLog != null)
{ {
try try
{ {
Console.WriteLine($"Creating reproducible, redump-style IRD with key from: {opt.GetKeyLog}"); Console.WriteLine($"Creating {irdPath} with key from: {getKeyLog}");
IRD ird3 = new ReIRD(isoPath, opt.GetKeyLog); IRD ird3 = new ReIRD(isoPath, getKeyLog);
ird3.Write(irdPath ?? Path.GetFileNameWithoutExtension(isoPath) + ".ird"); ird3.Write(irdPath);
ird3.Print(); ird3.Print();
return irdPath;
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
Console.Error.WriteLine("File not found"); 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(keyfilePath);
if (discKey == null || discKey.Length != 16)
throw new ArgumentException(keyfilePath);
Console.WriteLine($"Creating {irdPath} with Key: {Convert.ToHexString(discKey)}");
IRD ird2 = new ReIRD(isoPath, discKey, layerbreak);
ird2.Write(irdPath);
ird2.Print();
return irdPath;
}
catch (ArgumentException)
{
Console.Error.WriteLine("Given key file not valid, detecting key automatically...");
}
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: {logfilePath}");
IRD ird3 = new ReIRD(isoPath, logfilePath);
ird3.Write(irdPath);
ird3.Print();
return irdPath;
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("File not found, failed to create IRD");
return null;
} }
return;
} }
// No key provided, try get key from redump.org // No key provided, try get key from redump.org
Console.WriteLine("No key provided... Searching for key on redump.org..."); Console.WriteLine("No key provided... Searching for key on redump.org");
// Compute CRC32 hash // Compute CRC32 hash
byte[] crc32; byte[] crc32;
@@ -273,8 +737,8 @@ namespace IRDKit
int id; int id;
if (ids.Count == 0) if (ids.Count == 0)
{ {
Console.WriteLine("ISO not found in redump, cannot automatically retreive key."); Console.WriteLine("ISO not found in redump, cannot automatically retreive key");
return; return null;
} }
else if (ids.Count > 1) else if (ids.Count > 1)
{ {
@@ -291,13 +755,13 @@ namespace IRDKit
List<int> ids2 = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + sha1_hash).ConfigureAwait(false).GetAwaiter().GetResult(); List<int> ids2 = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + sha1_hash).ConfigureAwait(false).GetAwaiter().GetResult();
if (ids2.Count == 0) if (ids2.Count == 0)
{ {
Console.WriteLine("ISO not found in redump, cannot automatically retreive key."); Console.WriteLine("ISO not found in redump, cannot automatically retreive key");
return; return null;
} }
else if (ids2.Count > 1) else if (ids2.Count > 1)
{ {
Console.WriteLine("Cannot automatically get key from redump. Please search redump.org and run again with -k"); Console.WriteLine("Cannot automatically get key from redump. Please search redump.org and run again with -k");
return; return null;
} }
id = ids2[0]; id = ids2[0];
} }
@@ -314,10 +778,31 @@ namespace IRDKit
} }
// Create IRD with key from redump // Create IRD with key from redump
Console.WriteLine($"Creating reproducible, redump-style IRD with Key: {Convert.ToHexString(key)}"); Console.WriteLine($"Creating {irdPath} with Key: {Convert.ToHexString(key)}");
IRD ird = new ReIRD(isoPath, key, opt.Layerbreak); IRD ird = new ReIRD(isoPath, key, layerbreak);
ird.Write(irdPath ?? Path.GetFileNameWithoutExtension(isoPath) + ".ird"); ird.Write(irdPath);
ird.Print(); ird.Print();
return irdPath;
} }
#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
} }
} }
+26
View File
@@ -7,6 +7,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibIRD", "LibIRD\LibIRD.csp
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRDKit", "IRDKit\IRDKit.csproj", "{9060E3AF-E4FB-436F-93A1-5C47389CB88E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRDKit", "IRDKit\IRDKit.csproj", "{9060E3AF-E4FB-436F-93A1-5C47389CB88E}"
EndProject EndProject
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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -21,6 +35,18 @@ Global
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+128 -94
View File
@@ -1,12 +1,12 @@
using DiscUtils; using DiscUtils;
using DiscUtils.Iso9660; using DiscUtils.Iso9660;
using DiscUtils.Streams;
using System; using System;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.IO.Hashing; using System.IO.Hashing;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json;
namespace LibIRD namespace LibIRD
{ {
@@ -365,13 +365,12 @@ namespace LibIRD
/// <param name="discKey">Disc Key, byte array of length 16</param> /// <param name="discKey">Disc Key, byte array of length 16</param>
/// <param name="discID">Disc ID, 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> /// <param name="discPIC">Disc PIC, byte array of length 115</param>
/// <param name="redump">True if redump-style IRD</param> /// <param name="redump">True if redump-style IRD (default: false)</param>
public IRD(string isoPath, byte[] discKey, byte[] discID, byte[] discPIC, bool redump = false) public IRD(string isoPath, byte[] discKey, byte[] discID, byte[] discPIC, bool redump = false)
{ {
// Parse ISO, Disc Key, Disc ID, and PIC // Parse ISO, Disc Key, Disc ID, and PIC
DiscKey = discKey; DiscKey = discKey;
GenerateD1(discKey); DiscID = discID;
GenerateD2(discID);
PIC = discPIC; PIC = discPIC;
// Generate IRD files from ISO // Generate IRD files from ISO
@@ -646,29 +645,54 @@ namespace LibIRD
// Parse PS3_DISC.SFB file // Parse PS3_DISC.SFB file
PS3_DiscSFB ps3_DiscSFB = new(s); PS3_DiscSFB ps3_DiscSFB = new(s);
bool title_id_found = ps3_DiscSFB.Field.TryGetValue("TITLE_ID", out string title_id); 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 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] == '-') if (titleIDFound && titleID.Length == 10 && titleID[4] == '-')
TitleID = string.Concat(title_id.AsSpan(0, 4), title_id.AsSpan(5, 5)); TitleID = string.Concat(titleID.AsSpan(0, 4), titleID.AsSpan(5, 5));
// If the version field is present, this is a multi-game disc // 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 // 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); bool discVersionFound = ps3_DiscSFB.Field.TryGetValue("VERSION", out string discVersion);
if (version_found) if (discVersionFound)
DiscVersion = disc_version; DiscVersion = discVersion;
} }
// Read PS3 Metadata from PARAM.SFO // Read PS3 Metadata from PARAM.SFO
using (DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read)) using (DiscUtils.Streams.SparseStream s = reader.OpenFile("\\PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read))
{ {
// Parse PARAM.SFO file // Parse PARAM.SFO file
ParamSFO paramSFO = new(s); ParamSFO paramSFO = new(s);
// If PS3_DISC.SFB did not set TitleID, use PARAM.SFO TITLE_ID // If PS3_DISC.SFB did not set TitleID, use PARAM.SFO TITLE_ID
TitleID ??= paramSFO.Field["TITLE_ID"]; if (TitleID == null)
Title = paramSFO.Field["TITLE"]; {
// If PS3_DISC.SFB did not set DiscVersion, use PARAM.SFO VERSION bool titleIDFound = paramSFO.Field.TryGetValue("TITLE_ID", out string titleID);
DiscVersion ??= paramSFO.Field["VERSION"]; if (titleIDFound)
AppVersion = paramSFO.Field["APP_VER"]; 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 // Determine system update version
@@ -697,7 +721,12 @@ namespace LibIRD
FileCount = 0; FileCount = 0;
HashFiles(fs, reader, rootDir); HashFiles(fs, reader, rootDir);
if (FileCount != fileCount) if (FileCount != fileCount)
throw new InvalidFileSystemException("Unexpected ISO filesystem error: "); {
Console.WriteLine($"Likely contains non-contiguous files: detected {FileCount} out of {fileCount} expected files");
long[] tempFileKeys = FileKeys;
Array.Resize(ref tempFileKeys, (int)FileCount);
FileKeys = tempFileKeys;
}
Array.Sort(FileKeys, FileHashes); Array.Sort(FileKeys, FileHashes);
} }
@@ -716,32 +745,21 @@ namespace LibIRD
{ {
// Determine PUP file offset via cluster // Determine PUP file offset via cluster
DiscUtils.Streams.Range<long, long>[] updateClusters = reader.PathToClusters("\\PS3_UPDATE\\PS3UPDAT.PUP"); DiscUtils.Streams.Range<long, long>[] updateClusters = reader.PathToClusters("\\PS3_UPDATE\\PS3UPDAT.PUP");
if (updateClusters == null || updateClusters.Length <= 0) if (updateClusters == null && updateClusters.Length == 0 && updateClusters[0] == null)
{ throw new InvalidFileSystemException("Invalid file extents for PS3UPDAT.PUP");
// 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"); // PS3UPDAT.PUP file begins at first byte of dedicated cluster
if (updateExtents == null || updateExtents.Length <= 0) UpdateOffset = SectorSize * updateClusters[0].Offset;
throw new InvalidFileSystemException("Unexpected PS3UPDAT.PUP file extent in ISO filestream"); // Update file ends at the last byte of the last cluster
// PS3UPDAT.PUP file begins at start of first extent UpdateEnd = SectorSize * updateClusters[^1].Offset + updateClusters[^1].Count;
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);
}
// Check PUP file Magic // Check PUP file Magic
fs.Seek(UpdateOffset, SeekOrigin.Begin); fs.Seek(UpdateOffset, SeekOrigin.Begin);
byte[] pupMagic = new byte[5]; byte[] pupMagic = new byte[5];
fs.Read(pupMagic, 0, pupMagic.Length); 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") if (Encoding.ASCII.GetString(pupMagic) != "SCEUF")
SystemVersion = "0000"; SystemVersion = "\0\0\0\0";
else else
{ {
// Determine location of version string // Determine location of version string
@@ -770,19 +788,10 @@ namespace LibIRD
{ {
// Determine the extent of the header via cluster (Sector 0 to first data sector) // 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"); DiscUtils.Streams.Range<long, long>[] sfbClusters = reader.PathToClusters("\\PS3_DISC.SFB");
if (sfbClusters == null || sfbClusters.Length <= 0) if (sfbClusters == null && sfbClusters.Length == 0 && sfbClusters[0] == null)
{ throw new InvalidFileSystemException("Invalid file extents for PS3_DISC.SFB");
// File too small for dedicated cluster, try get the first sector from the file extents instead // End of header is at beginning of first byte of dedicated cluster
DiscUtils.Streams.StreamExtent[] sfbExtents = reader.PathToExtents("\\PS3_DISC.SFB"); FirstDataSector = sfbClusters[0].Offset;
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;
}
// Begin a GZip stream to write header to // Begin a GZip stream to write header to
using MemoryStream headerStream = new(); using MemoryStream headerStream = new();
@@ -816,7 +825,7 @@ namespace LibIRD
using MemoryStream footerStream = new(); using MemoryStream footerStream = new();
using (GZipStream gzs = new(footerStream, CompressionLevel.SmallestSize)) using (GZipStream gzs = new(footerStream, CompressionLevel.SmallestSize))
{ {
// Start reading data from after last file // Start reading data from after last file (PS3UPDAT.PUP)
fs.Seek(UpdateEnd, SeekOrigin.Begin); fs.Seek(UpdateEnd, SeekOrigin.Begin);
byte[] buf = new byte[SectorSize]; byte[] buf = new byte[SectorSize];
int numBytes = (int)SectorSize; int numBytes = (int)SectorSize;
@@ -919,15 +928,30 @@ namespace LibIRD
{ {
string filePath = fileInfo.FullName; string filePath = fileInfo.FullName;
// Try get the first sector from the file extents instead // Determine the extents of the file via clusters
DiscUtils.Streams.StreamExtent[] fileExtents = reader.PathToExtents(filePath); DiscUtils.Streams.Range<long, long>[] fileClusters = reader.PathToClusters(filePath);
if (fileExtents == null || fileExtents.Length <= 0)
throw new InvalidFileSystemException("Unexpected file extent in ISO filestream for " + filePath); // If invalid clusters were returned, we can't hash this file
if (fileExtents.Length > 1) if (fileClusters == null && fileClusters.Length == 0)
throw new InvalidFileSystemException("Non-contiguous file detected"); throw new InvalidFileSystemException($"Unexpected file extents for {filePath}");
long firstByte = fileExtents[0].Start;
int firstSector = (int)(firstByte / 2048); // Determine smallest file offset as first sector
long fileLength = fileExtents[0].Length; 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;
}
int firstSector = (int)(smallestOffset);
// If already encountered file offset, skip this file
if (Array.Exists(FileKeys, element => element == firstSector))
continue;
// Add file offset to keys
FileKeys[FileCount] = firstSector; FileKeys[FileCount] = firstSector;
// Determine whether file is in encrypted or decrypted region // Determine whether file is in encrypted or decrypted region
@@ -941,36 +965,40 @@ namespace LibIRD
} }
} }
// Start reading data from the beginning of the ISO file // Hash each non-contiguous portion of the ISO file
fs.Seek(firstByte, SeekOrigin.Begin);
byte[] buf = new byte[SectorSize]; byte[] buf = new byte[SectorSize];
int numBytes;
// Read all data before the first data sector
MD5 md5 = MD5.Create(); 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); // Start reading data from the beginning of the ISO file
// Check that an entire sector was read fs.Seek(fileClusters[i].Offset * SectorSize, SeekOrigin.Begin);
if (numBytes < buf.Length) int numBytes;
throw new InvalidFileSystemException("Disc region ended unexpectedly"); // Read all data before the first data sector
// Decrypt sector if necessary for (int j = 0; j < (fileClusters[i].Count / SectorSize); j++)
if (encrypted) {
buf = DecryptSector(buf, firstSector + i); numBytes = fs.Read(buf, 0, buf.Length);
// Hash sector // Check that an entire sector was read
md5.TransformBlock(buf, 0, numBytes, null, 0); if (numBytes < buf.Length)
} throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Read remaining partial sector // Decrypt sector if necessary
if (fileLength % SectorSize != 0) if (encrypted)
{ buf = DecryptSector(buf, firstSector + j);
numBytes = fs.Read(buf, 0, buf.Length); // Hash sector
// Check that an entire sector was read md5.TransformBlock(buf, 0, numBytes, null, 0);
if (numBytes < buf.Length) }
throw new InvalidFileSystemException("Disc region ended unexpectedly"); // Read remaining partial sector
// Decrypt partial sector if necessary if (fileClusters[i].Count % SectorSize != 0)
if (encrypted) {
buf = DecryptSector(buf, firstSector + (int)(fileLength / SectorSize)); numBytes = fs.Read(buf, 0, buf.Length);
// Hash partial sector // Check that an entire sector was read
md5.TransformBlock(buf, 0, (int)(fileLength % SectorSize), null, 0); if (numBytes < buf.Length)
throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Decrypt partial sector if necessary
if (encrypted)
buf = DecryptSector(buf, firstSector + (int)(fileClusters[i].Count / SectorSize));
// Hash partial sector
md5.TransformBlock(buf, 0, (int)(fileClusters[i].Count % SectorSize), null, 0);
}
} }
// Finalise and store MD5 hash // Finalise and store MD5 hash
@@ -1239,7 +1267,7 @@ namespace LibIRD
// Read UID (for Version 8 onwards) // Read UID (for Version 8 onwards)
if (version > 7) if (version > 7)
uid = br.ReadUInt16(); uid = br.ReadUInt32();
// Read and CRC32 hash // Read and CRC32 hash
byte[] crc = br.ReadBytes(4); byte[] crc = br.ReadBytes(4);
@@ -1268,11 +1296,14 @@ namespace LibIRD
/// Prints IRD fields to console /// Prints IRD fields to console
/// </summary> /// </summary>
/// <param name="printPath">Optional path to save file to</param> /// <param name="printPath">Optional path to save file to</param>
public void Print(string printPath = null) public void Print(string printPath = null, string irdName = null)
{ {
// Build string from parameters // Build string from parameters
StringBuilder printText = new(); StringBuilder printText = new();
printText.AppendLine("IRD Contents:"); if (irdName == null)
printText.AppendLine("IRD Contents:");
else
printText.AppendLine($"IRD Contents: {irdName}");
printText.AppendLine("============="); printText.AppendLine("=============");
// Append IRD fields to string builder // Append IRD fields to string builder
@@ -1305,7 +1336,7 @@ namespace LibIRD
} }
else else
{ {
File.WriteAllText(printPath, printText.ToString()); File.AppendAllText(printPath, printText.ToString());
} }
} }
@@ -1314,7 +1345,7 @@ namespace LibIRD
/// Prints IRD fields to a json object /// Prints IRD fields to a json object
/// </summary> /// </summary>
/// <param name="jsonPath">Optionally print to json file</param> /// <param name="jsonPath">Optionally print to json file</param>
public void PrintJson(string jsonPath = null) public void PrintJson(string jsonPath = null, bool single = true)
{ {
// Build string from parameters // Build string from parameters
StringBuilder json = new(); StringBuilder json = new();
@@ -1334,11 +1365,14 @@ namespace LibIRD
json.AppendLine($" \"Extra Config\": \"{ExtraConfig:X4}\","); json.AppendLine($" \"Extra Config\": \"{ExtraConfig:X4}\",");
if (Attachments != 0x0000) if (Attachments != 0x0000)
json.AppendLine($" \"Attachments\": \"{Attachments:X4}\","); json.AppendLine($" \"Attachments\": \"{Attachments:X4}\",");
json.AppendLine($" \"Unique ID\": \"{UID:X8}"); json.AppendLine($" \"Unique ID\": \"{UID:X8}\",");
json.AppendLine($" \"Data 1 Key\": \"{Convert.ToHexString(Data1Key)}\","); json.AppendLine($" \"Data 1 Key\": \"{Convert.ToHexString(Data1Key)}\",");
json.AppendLine($" \"Data 2 Key\": \"{Convert.ToHexString(Data2Key)}\","); json.AppendLine($" \"Data 2 Key\": \"{Convert.ToHexString(Data2Key)}\",");
json.AppendLine($" \"PIC\": \"{Convert.ToHexString(PIC)}\""); json.AppendLine($" \"PIC\": \"{Convert.ToHexString(PIC)}\"");
json.AppendLine("}"); if (single)
json.AppendLine("}");
else
json.AppendLine("},");
// If no path given, output to console // If no path given, output to console
if (jsonPath == null) if (jsonPath == null)
+7 -6
View File
@@ -6,27 +6,28 @@
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>0.2.0</Version> <Version>0.3.0</Version>
<!-- Package Properties --> <!-- Package Properties -->
<Authors>Deterous</Authors> <Authors>Deterous</Authors>
<Description>Library for ISO Rebuild Data</Description> <Description>Library for ISO Rebuild Data</Description>
<Copyright>Copyright (c) Deterous 2023</Copyright> <Copyright>Copyright (c) Deterous 2024</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/Deterous/LibIRD/</RepositoryUrl> <RepositoryUrl>https://github.com/Deterous/LibIRD/</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>ps3 iso ird redump</PackageTags> <PackageTags>ps3 iso ird redump</PackageTags>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="./README.md" Pack="true" PackagePath=""/> <None Include="./README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DiscUtils.Core" Version="0.16.13" /> <ProjectReference Include="..\DiscUtils\Library\DiscUtils.Core\DiscUtils.Core.csproj" />
<PackageReference Include="DiscUtils.Iso9660" Version="0.16.13" /> <ProjectReference Include="..\DiscUtils\Library\DiscUtils.Iso9660\DiscUtils.Iso9660.csproj" />
<PackageReference Include="DiscUtils.Streams" Version="0.16.13" /> <ProjectReference Include="..\DiscUtils\Library\DiscUtils.Streams\DiscUtils.Streams.csproj" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" /> <PackageReference Include="System.IO.Hashing" Version="8.0.0" />
</ItemGroup> </ItemGroup>
+5 -2
View File
@@ -103,11 +103,14 @@ namespace LibIRD
/// Prints formatted parameters extracted from PS3_DISC.SFB to console /// Prints formatted parameters extracted from PS3_DISC.SFB to console
/// </summary> /// </summary>
/// <param name="printPath">Optionally print to text file</param> /// <param name="printPath">Optionally print to text file</param>
public void Print(string printPath = null) public void Print(string printPath = null, string isoName = null)
{ {
// Build string from parameters // Build string from parameters
StringBuilder printText = new(); StringBuilder printText = new();
printText.AppendLine("PS3_DISC.SFB Contents:"); if (isoName != null)
printText.AppendLine($"PS3_DISC.SFB Contents: {isoName}");
else
printText.AppendLine("PS3_DISC.SFB Contents:");
printText.AppendLine("======================"); printText.AppendLine("======================");
// Loop through all parameters in PARAM.SFO // Loop through all parameters in PARAM.SFO
+6 -3
View File
@@ -127,12 +127,15 @@ namespace LibIRD
/// Prints formatted parameters extracted from PARAM.SFO to console /// Prints formatted parameters extracted from PARAM.SFO to console
/// </summary> /// </summary>
/// <param name="printPath">Optionally print to text file</param> /// <param name="printPath">Optionally print to text file</param>
public void Print(string printPath = null) public void Print(string printPath = null, string isoName = null)
{ {
// Build string from parameters // Build string from parameters
StringBuilder printText = new(); StringBuilder printText = new();
printText.AppendLine("PARAM.SFO Contents:"); if (isoName != null)
printText.AppendLine("===================="); printText.AppendLine($"PARAM.SFO Contents: {isoName}");
else
printText.AppendLine("PARAM.SFO Contents:");
printText.AppendLine("===================");
// Loop through all parameters in PARAM.SFO // Loop through all parameters in PARAM.SFO
foreach (KeyValuePair<string, string> field in Field) foreach (KeyValuePair<string, string> field in Field)
+1 -1
View File
@@ -95,7 +95,7 @@ namespace LibIRD
/// <param name="key">Disc Key, redump-style (AES encrypted Data 1)</param> /// <param name="key">Disc Key, redump-style (AES encrypted Data 1)</param>
/// <param name="layerbreak">Layerbreak value, in sectors</param> /// <param name="layerbreak">Layerbreak value, in sectors</param>
/// <param name="region">Disc Region</param> /// <param name="region">Disc Region</param>
public ReIRD(string isoPath, byte[] key, long? layerbreak = null, Region region = Region.NONE) public ReIRD(string isoPath, byte[] key, long? layerbreak = null, Region region = Region.NONE) : base()
{ {
// Generate Unique Identifier using ISO CRC32 // Generate Unique Identifier using ISO CRC32
UID = GenerateUID(isoPath); UID = GenerateUID(isoPath);
+5 -8
View File
@@ -8,16 +8,13 @@ IRD files contain a summary of what data is on a PlayStation 3 disc. It can be u
## How to use IRDKit ## 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 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`
irdkit create 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). For detailed usage, read more [here](IRDKit).
## Using the LibIRD library ## 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). 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.