diff --git a/IRDKit/Program.cs b/IRDKit/Program.cs
index 0bac339..ce8e6b1 100644
--- a/IRDKit/Program.cs
+++ b/IRDKit/Program.cs
@@ -6,6 +6,7 @@ using SabreTools.RedumpLib.Web;
using System;
using System.Collections.Generic;
using System.IO;
+using System.IO.Compression;
using System.IO.Hashing;
using System.Linq;
using System.Security.Cryptography;
@@ -15,6 +16,8 @@ namespace IRDKit
{
internal class Program
{
+ #region Options
+
///
/// IRD Creation command
///
@@ -62,6 +65,26 @@ namespace IRDKit
public bool Recurse { get; set; }
}
+ ///
+ /// IRD diff command
+ ///
+ [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
+
///
/// Parse command line arguments
///
@@ -72,7 +95,7 @@ namespace IRDKit
Console.OutputEncoding = Encoding.UTF8;
// Parse arguments
- var result = Parser.Default.ParseArguments(args).WithParsed(Run);
+ var result = Parser.Default.ParseArguments(args).WithParsed(Run);
}
///
@@ -151,12 +174,17 @@ namespace IRDKit
// Process options from an `info` command
case InfoOptions opt:
+ // 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)
{
+ // Validate path
+ ArgumentNullException.ThrowIfNull(filePath);
// If directory, search for all ISOs in current directory
if (Directory.Exists(filePath))
@@ -248,12 +276,37 @@ namespace IRDKit
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;
}
}
+ #endregion
+
+ #region Functionality
+
///
/// Prints info about a file
///
@@ -329,7 +382,7 @@ namespace IRDKit
// Write PS3_DISC.SFB info
try
{
- using DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_DISC.SFB", FileMode.Open, FileAccess.Read);
+ using DiscUtils.Streams.SparseStream s = reader.OpenFile(Path.Combine($"{Path.DirectorySeparatorChar}", "PS3_DISC.SFB"), FileMode.Open, FileAccess.Read);
PS3_DiscSFB ps3_DiscSFB = new(s);
if (json)
{
@@ -405,6 +458,110 @@ namespace IRDKit
}
}
+ ///
+ /// Prints the differences between two IRD files
+ ///
+ /// First IRD path to compare against
+ /// Second IRD path to compare against
+ /// File to write comparison to, null if print to Console
+ 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;
+ 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}");
+
+ uint fileCount = IRD2.FileCount < IRD1.FileCount ? IRD2.FileCount : IRD1.FileCount;
+ 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());
+ }
+
///
/// Creates an IRD file from an ISO file
///
@@ -615,5 +772,25 @@ namespace IRDKit
ird.Print();
return irdPath;
}
+
+ #endregion
+
+ #region Helper Functions
+
+ ///
+ /// Decompress a gzipped byte array
+ ///
+ /// Gzipped byte array
+ /// Un-gzipped byte array
+ 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
}
}
diff --git a/LibIRD/IRD.cs b/LibIRD/IRD.cs
index 73f1b14..6a7b441 100644
--- a/LibIRD/IRD.cs
+++ b/LibIRD/IRD.cs
@@ -658,7 +658,7 @@ namespace LibIRD
}
// Read PS3 Metadata from PARAM.SFO
- using (DiscUtils.Streams.SparseStream s = reader.OpenFile(Path.Combine("PS3_GAME", "PARAM.SFO"), FileMode.Open, FileAccess.Read))
+ using (DiscUtils.Streams.SparseStream s = reader.OpenFile(Path.Combine($"{Path.DirectorySeparatorChar}", "PS3_GAME", "PARAM.SFO"), FileMode.Open, FileAccess.Read))
{
// Parse PARAM.SFO file
ParamSFO paramSFO = new(s);
@@ -710,7 +710,7 @@ namespace LibIRD
// TODO: Speed up program by hashing regions and files at the same time (read from filesystem only once)
// Recursively count all files in ISO to allocate file arrays
- DiscDirectoryInfo rootDir = reader.GetDirectoryInfo("");
+ DiscDirectoryInfo rootDir = reader.GetDirectoryInfo($"{Path.DirectorySeparatorChar}");
FileCount = 0;
CountFiles(rootDir);
FileKeys = new long[FileCount];
@@ -739,7 +739,7 @@ namespace LibIRD
private void GetSystemVersion(FileStream fs, CDReader reader)
{
// Determine PUP file offset via cluster
- DiscUtils.Streams.Range[] updateClusters = reader.PathToClusters(Path.Combine("PS3_UPDATE", "PS3UPDAT.PUP"));
+ DiscUtils.Streams.Range[] updateClusters = reader.PathToClusters(Path.Combine($"{Path.DirectorySeparatorChar}", "PS3_UPDATE", "PS3UPDAT.PUP"));
if (updateClusters == null && updateClusters.Length == 0 && updateClusters[0] == null)
throw new InvalidFileSystemException("Invalid file extents for PS3UPDAT.PUP");