32 Commits
Author SHA1 Message Date
Deterous fed9d6ea57 Trim DiscUtils further 2024-03-06 14:14:37 +09:00
Deterous 41946b6bc5 Use trimmed DiscUtils 2024-03-06 12:09:30 +09:00
Deterous d585c4b534 Update ST.Hashing version 2024-03-06 12:09:15 +09:00
Deterous 1886e14a9b Use a trimmed DiscUtils 2024-03-05 23:34:38 +09:00
Deterous e8f4d88711 Use ST.Hashing for SHA1 2024-03-05 15:15:40 +09:00
Deterous 0dbc483a91 Use ST.Hashing for MD5 2024-03-05 15:02:27 +09:00
Deterous b6bb5f0582 Attempt to support net40 with ST.Hashing 2024-03-05 14:52:30 +09:00
Deterous ea3f6d3f96 Support .NET Framework using Newtonsoft 2024-03-05 13:02:25 +09:00
Deterous 4aa6aca25e Support building LibIRD with older .NET Core 2024-03-05 12:33:18 +09:00
Deterous a93c727ba2 Fix spacing 2024-03-05 12:11:34 +09:00
Deterous 55cab8202d Subsume used DiscUtils libraries 2024-03-05 12:01:25 +09:00
Deterous 1fa7380f06 Move DiscUtils to within LibIRD subfolder 2024-03-05 11:01:54 +09:00
DeterousandGitHub a0acd91a55 Update README.md 2024-03-01 12:35:06 +09:00
Deterous 6edb959e49 Bump IRDKit to v0.6.1 2024-02-27 13:24:08 +09:00
Deterous 96b36fcabe Add rename functionality 2024-02-27 12:49:14 +09:00
Deterous 9d03dbb7b1 Perform hashing in one read pass 2024-02-20 16:37:15 +09:00
Deterous c7b49d0c80 Initial work towards one-pass hashing 2024-02-19 21:38:06 +09:00
Deterous 5ce81f9663 Bump version 2024-02-18 17:28:21 +09:00
Deterous f74220efea Read ISO in larger chunks 2024-02-18 16:59:04 +09:00
Deterous 6e529f0050 Reuse ISO hash if searching redump 2024-02-16 11:11:51 +09:00
Deterous b862057123 Only print warning for non-contiguous files 2024-02-11 12:39:13 +09:00
Deterous cf19596ea7 Alphabetical order, key file path 2024-02-08 09:38:46 +09:00
Deterous 6af41a9bb3 Include dependencies in nupkg 2024-02-07 15:43:23 +09:00
Deterous 89c23622bd Remove DiscUtils as a public dependency 2024-02-07 14:41:14 +09:00
Deterous 8ab6a9d694 Fix file hash for split files with irregular filesize 2024-02-07 09:32:56 +09:00
Deterous bc3df52e75 Cleanup 2024-02-05 09:20:59 +09:00
Deterous 23f8dce272 Bump version, provide build script 2024-02-04 12:09:11 +09:00
Deterous 8e45432aab Throw error for BD-Video hybrid discs without layerbreak 2024-02-03 23:15:13 +09:00
Deterous e9bb0ae00a Allow for output folder argument, bump version 2024-02-01 10:55:23 +09:00
Deterous 4116c56b7d Add verbose flag to IRDKit, improve printing 2024-02-01 10:31:07 +09:00
Deterous e30ac0961a Finalise diff function, bump version 2024-02-01 00:52:35 +09:00
Deterous 1ef9f06e35 Improve diff 2024-01-31 23:41:46 +09:00
14 changed files with 1139 additions and 627 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "DiscUtils"]
path = DiscUtils
path = LibIRD/DiscUtils
url = https://github.com/Deterous/DiscUtils
Submodule DiscUtils deleted from f53a1dab61
+13 -10
View File
@@ -3,19 +3,20 @@
<PropertyGroup>
<!-- Assembly Properties -->
<OutputType>Exe</OutputType>
<PackAsTool>true</PackAsTool>
<ToolCommandName>irdkit</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<TargetName>irdkit</TargetName>
<AssemblyName>irdkit</AssemblyName>
<TargetFrameworks>net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<LangVersion>latest</LangVersion>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>0.3.0</Version>
<Version>0.7.0</Version>
<!-- Package Properties -->
<Authors>Deterous</Authors>
<Description>Library for ISO Rebuild Data</Description>
<Copyright>Copyright (c) Deterous 2023</Copyright>
<Copyright>Copyright (c) Deterous 2023-2024</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/Deterous/LibIRD</RepositoryUrl>
<RepositoryType>git</RepositoryType>
@@ -28,11 +29,13 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LibIRD\LibIRD.csproj" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Core\DiscUtils.Core.csproj" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Iso9660\DiscUtils.Iso9660.csproj" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="SabreTools.RedumpLib" Version="1.3.2" />
<PackageReference Include="SabreTools.Hashing" Version="1.1.3" />
<PackageReference Include="SabreTools.RedumpLib" Version="1.3.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LibIRD\LibIRD.csproj" />
</ItemGroup>
</Project>
+430 -146
View File
@@ -1,16 +1,14 @@
using CommandLine;
using DiscUtils;
using DiscUtils.Iso9660;
using LibIRD;
using SabreTools.RedumpLib.Web;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Hashing;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Xml.Linq;
using CommandLine;
using LibIRD;
using SabreTools.Hashing;
using SabreTools.RedumpLib.Web;
namespace IRDKit
{
@@ -24,7 +22,7 @@ namespace IRDKit
[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")]
[Value(0, Required = true, HelpText = "Path to ISO file(s), or directory of ISO files")]
public IEnumerable<string> ISOPath { get; set; }
[Option('o', "output", HelpText = "Path to the IRD file to be created (will overwrite)")]
@@ -44,6 +42,9 @@ namespace IRDKit
[Option('r', "recurse", HelpText = "Recurse through all subdirectories and generate IRDs for all ISOs")]
public bool Recurse { get; set; }
[Option('v', "verbose", HelpText = "Print more information during IRD creation")]
public bool Verbose { get; set; }
}
/// <summary>
@@ -52,7 +53,7 @@ namespace IRDKit
[Verb("info", HelpText = "Print information from an IRD or ISO")]
public class InfoOptions
{
[Value(0, Required = true, HelpText = "Path to an IRD or ISO file, or directory of IRD and/or ISO files")]
[Value(0, Required = true, HelpText = "Path to IRD/ISO file(s), or directory of IRD/ISO files")]
public IEnumerable<string> InPath { get; set; }
[Option('o', "output", HelpText = "Path to the text or json file to be created (will overwrite)")]
@@ -81,6 +82,31 @@ namespace IRDKit
public string OutPath { get; set; }
}
/// <summary>
/// IRD rename command
/// </summary>
[Verb("rename", HelpText = "Rename one or more IRD files according to the redump PS3 DAT")]
public class RenameOptions
{
[Value(0, Required = true, HelpText = "Path to IRD file(s), or directory of IRD files")]
public IEnumerable<string> IRDPath { get; set; }
[Option('d', "datfile", Required = true, HelpText = "Path to the redump PS3 Datfile")]
public string DATPath { get; set; }
[Option('s', "serial", HelpText = "Appends disc serial to new IRD filename")]
public bool Serial { get; set; }
[Option('c', "crc", HelpText = "Appends ISO CRC to new IRD filename")]
public bool CRC { get; set; }
[Option('r', "recurse", HelpText = "Recurse through all subdirectories and rename all IRDs")]
public bool Recurse { get; set; }
[Option('v', "verbose", HelpText = "Print more information about the renaming")]
public bool Verbose { get; set; }
}
#endregion
#region Program
@@ -95,7 +121,7 @@ namespace IRDKit
Console.OutputEncoding = Encoding.UTF8;
// Parse arguments
var result = Parser.Default.ParseArguments<CreateOptions, InfoOptions, DiffOptions>(args).WithParsed(Run);
var result = Parser.Default.ParseArguments<CreateOptions, InfoOptions, DiffOptions, RenameOptions>(args).WithParsed(Run);
}
/// <summary>
@@ -111,12 +137,17 @@ namespace IRDKit
case CreateOptions opt:
// Validate ISO paths
ArgumentNullException.ThrowIfNull(opt.ISOPath);
if (opt.ISOPath == null || !opt.ISOPath.Any())
{
Console.Error.WriteLine("Provide a valid ISO path to create an IRD");
return;
}
foreach (string isoPath in opt.ISOPath)
{
// Validate ISO path
ArgumentNullException.ThrowIfNull(isoPath);
if (string.IsNullOrEmpty(isoPath))
continue;
// If directory, search for all ISOs in current directory
if (Directory.Exists(isoPath))
@@ -125,49 +156,61 @@ namespace IRDKit
IEnumerable<string> isoFiles;
if (opt.Recurse)
{
if (isoPath == ".")
if (opt.Verbose && isoPath == ".")
Console.WriteLine($"Recursively searching for ISOs in current directory");
else
else if (opt.Verbose)
Console.WriteLine($"Recursively searching for ISOs in {isoPath}");
isoFiles = Directory.EnumerateFiles(isoPath, "*.iso", SearchOption.AllDirectories);
}
else
{
if (isoPath == ".")
if (opt.Verbose && isoPath == ".")
Console.WriteLine($"Searching for ISOs in current directory");
else
else if (opt.Verbose)
Console.WriteLine($"Searching for ISOs in {isoPath}");
isoFiles = Directory.EnumerateFiles(isoPath, "*.iso", SearchOption.TopDirectoryOnly);
}
// Warn if no files are found
if (!isoFiles.Any())
Console.WriteLine("No ISOs found (ensure .iso extension)");
{
if (opt.Recurse)
Console.Error.WriteLine($"No ISOs found in {isoPath} (ensure .iso extension)");
else
Console.Error.WriteLine($"No ISOs found in {isoPath} (ensure .iso extension, or try use -r)");
continue;
}
// Determine output IRD folder
string outputPath = opt.IRDPath;
if (File.Exists(opt.IRDPath))
outputPath = Path.GetDirectoryName(opt.IRDPath);
// Create an IRD file for all ISO files found
foreach (string file in isoFiles)
ISO2IRD(file);
foreach (string file in isoFiles.OrderBy(x => x))
ISO2IRD(file, irdPath: outputPath, keyPath: opt.KeyFile, verbose: opt.Verbose);
}
else
{
// Check that given file exists
if (!File.Exists(isoPath)) throw new ArgumentException("Not a valid file or directory");
if (!File.Exists(isoPath))
{
Console.Error.WriteLine($"ISO not found: {isoPath}");
continue;
}
// 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}");
}
string irdPath;
// Save to given output path and filename, if only 1 IRD is being created
if (opt.ISOPath.Count() == 1)
irdPath = ISO2IRD(isoPath, irdPath: opt.IRDPath, hexKey: opt.Key, keyPath: opt.KeyFile, getKeyLog: opt.GetKeyLog, layerbreak: opt.Layerbreak, verbose: opt.Verbose);
// Save to given output path, if more than 1 IRD is being created
else
{
string irdPath = ISO2IRD(isoPath, null, opt.Key, opt.KeyFile, opt.GetKeyLog, opt.Layerbreak);
irdPath = ISO2IRD(isoPath, irdPath: Path.GetDirectoryName(opt.IRDPath), verbose: opt.Verbose);
if (irdPath != null)
Console.WriteLine($"IRD saved to {irdPath}");
}
}
}
break;
@@ -175,7 +218,11 @@ namespace IRDKit
case InfoOptions opt:
// Validate required parameter
ArgumentNullException.ThrowIfNull(opt.InPath);
if (opt.InPath == null || !opt.InPath.Any())
{
Console.Error.WriteLine("Provide a valid ISO or IRD path to print info about");
return;
}
// Clear the output file path if it exists
if (opt.OutPath != null && opt.OutPath != "")
@@ -184,7 +231,8 @@ namespace IRDKit
foreach (string filePath in opt.InPath)
{
// Validate path
ArgumentNullException.ThrowIfNull(filePath);
if (string.IsNullOrEmpty(filePath))
continue;
// If directory, search for all ISOs in current directory
if (Directory.Exists(filePath))
@@ -213,7 +261,10 @@ namespace IRDKit
// Warn if no files are found
if (!isoFiles.Any() && !irdFiles.Any())
Console.WriteLine("No IRDs or ISOs found (ensure .ird and .iso extensions)");
{
Console.Error.WriteLine("No IRDs or ISOs found (ensure .ird and .iso extensions)");
return;
}
// Open JSON object
if (opt.Json)
@@ -241,11 +292,11 @@ namespace IRDKit
{
PrintISO(file, opt.Json, file.Equals(lastISO), opt.OutPath);
}
catch (InvalidFileSystemException)
catch (IOException)
{
// 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");
Console.Error.WriteLine($"{file} is not a valid ISO file\n");
}
}
@@ -264,7 +315,11 @@ namespace IRDKit
else
{
// Check that given file exists
if (!File.Exists(filePath)) throw new ArgumentException($"{filePath} is not a valid file or directory");
if (!File.Exists(filePath))
{
Console.Error.WriteLine($"{filePath} is not a valid file");
continue;
}
// Print info from given file
PrintInfo(filePath, opt.Json, true, opt.OutPath);
@@ -279,11 +334,12 @@ namespace IRDKit
// 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");
// Validate required parameters
if (opt.InPath1 == null || opt.InPath2 == null || !File.Exists(opt.InPath1) || !File.Exists(opt.InPath2))
{
Console.Error.WriteLine("Provide two paths to IRDs to compare");
return;
}
// Clear the output file path if it exists
if (opt.OutPath != null && opt.OutPath != "")
@@ -297,6 +353,99 @@ namespace IRDKit
break;
// Process options from a 'rename' command
case RenameOptions opt:
// Validate required parameters
if (opt.IRDPath == null || !opt.IRDPath.Any())
{
Console.Error.WriteLine("Provide a valid IRD path to rename");
return;
}
// Read DAT file
XDocument datfile = DatParser(opt.DATPath);
if (datfile == null)
{
Console.Error.WriteLine("Unable to parse DAT file");
return;
}
foreach (string irdPath in opt.IRDPath)
{
// Validate IRD path
if (string.IsNullOrEmpty(irdPath))
continue;
// If directory, search for all ISOs in current directory
if (Directory.Exists(irdPath))
{
// If recurse option enabled, search recursively
IEnumerable<string> irdFiles;
if (opt.Recurse)
{
if (opt.Verbose && irdPath == ".")
Console.WriteLine($"Recursively renaming IRDs in current directory");
else if (opt.Verbose)
Console.WriteLine($"Recursively renaming IRDs in {irdPath}");
irdFiles = Directory.EnumerateFiles(irdPath, "*.ird", SearchOption.AllDirectories);
}
else
{
if (opt.Verbose && irdPath == ".")
Console.WriteLine($"Renaming IRDs in current directory");
else if (opt.Verbose)
Console.WriteLine($"Renaming IRDs in {irdPath}");
irdFiles = Directory.EnumerateFiles(irdPath, "*.ird", SearchOption.TopDirectoryOnly);
}
// Warn if no files are found
if (!irdFiles.Any())
{
if (opt.Recurse)
Console.Error.WriteLine($"No IRDs found in {irdPath} (ensure .ird extension)");
else
Console.Error.WriteLine($"No IRDs found in {irdPath} (ensure .ird extension, or try use -r)");
continue;
}
// Rename all IRD files found
foreach (string file in irdFiles.OrderBy(x => x))
{
try
{
RenameIRD(file, datfile, serial: opt.Serial, crc: opt.CRC, verbose: opt.Verbose);
}
catch (Exception e)
{
Console.Error.WriteLine(e);
}
}
}
else
{
// Check that given file exists
if (!File.Exists(irdPath))
{
Console.Error.WriteLine($"IRD not found: {irdPath}");
continue;
}
// Rename provided IRD path
try
{
RenameIRD(irdPath, datfile, serial: opt.Serial, crc: opt.CRC, verbose: opt.Verbose);
}
catch (Exception e)
{
Console.Error.WriteLine(e);
}
}
}
break;
// Unknown command
default:
break;
@@ -324,7 +473,7 @@ namespace IRDKit
PrintISO(inPath, json, single, outPath);
return;
}
catch (InvalidFileSystemException)
catch (IOException)
{
// Not a valid ISO file despite extension, try open as IRD
}
@@ -355,9 +504,9 @@ namespace IRDKit
if (json)
return;
if (isISO)
Console.WriteLine($"{inPath} is not a valid ISO file\n");
Console.Error.WriteLine($"{inPath} is not a valid ISO file\n");
else
Console.WriteLine($"{inPath} is not a valid IRD file\n");
Console.Error.WriteLine($"{inPath} is not a valid IRD file\n");
}
}
@@ -372,17 +521,20 @@ namespace IRDKit
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);
using FileStream fs = new(isoPath, FileMode.Open, FileAccess.Read);
// Validate ISO file stream
if (!CDReader.Detect(fs))
throw new InvalidFileSystemException($"{isoPath} is not a valid ISO file");
if (fs == null || !LibIRD.DiscUtils.Iso9660.CDReader.Detect(fs))
{
Console.Error.WriteLine($"{isoPath} is not a valid ISO file");
return;
}
// Create new ISO reader
using CDReader reader = new(fs, true, true);
using LibIRD.DiscUtils.Iso9660.CDReader reader = new(fs);
// Write PS3_DISC.SFB info
try
{
using DiscUtils.Streams.SparseStream s = reader.OpenFile("\\PS3_DISC.SFB", FileMode.Open, FileAccess.Read);
using LibIRD.DiscUtils.Streams.SparseStream s = reader.OpenFile("\\PS3_DISC.SFB", FileMode.Open, FileAccess.Read);
PS3_DiscSFB ps3_DiscSFB = new(s);
if (json)
{
@@ -412,14 +564,14 @@ namespace IRDKit
catch (FileNotFoundException)
{
if (!json)
Console.WriteLine($"{isoPath} is not a valid PS3 ISO file\n");
Console.Error.WriteLine($"{isoPath} is not a valid PS3 ISO file\n");
return;
}
// Write PARAM.SFO info
try
{
using DiscUtils.Streams.SparseStream s = reader.OpenFile("\\PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read);
using LibIRD.DiscUtils.Streams.SparseStream s = reader.OpenFile("\\PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read);
ParamSFO paramSFO = new(s);
if (json)
{
@@ -435,7 +587,7 @@ namespace IRDKit
catch (FileNotFoundException)
{
if (!json)
Console.WriteLine($"\\PS3_GAME\\PARAM.SFO not found in {isoPath}\n");
Console.Error.WriteLine($"\\PS3_GAME\\PARAM.SFO not found in {isoPath}\n");
}
// End JSON object
@@ -469,7 +621,7 @@ namespace IRDKit
// Check they are different IRDs
if (Path.GetFullPath(irdPath1) == Path.GetFullPath(irdPath2))
{
Console.WriteLine("Provide two different IRDs for a diff");
Console.Error.WriteLine("Provide two different IRDs for a diff");
return;
}
@@ -480,45 +632,69 @@ namespace IRDKit
// Build a formatted diff
StringBuilder printText = new();
// Print any version difference
if (IRD1.Version != IRD2.Version)
printText.AppendLine($"Version: {IRD1.Version} vs {IRD2.Version}");
// Print any title ID difference
if (IRD1.TitleID != IRD2.TitleID)
printText.AppendLine($"TitleID: {IRD1.TitleID} vs {IRD2.TitleID}");
// Print any title difference
if (IRD1.Title != IRD2.Title)
printText.AppendLine($"Title: {IRD1.Title} vs {IRD2.Title}");
printText.AppendLine($"Title: \"{IRD1.Title}\" vs \"{IRD2.Title}\"");
// Print any system version difference
if (IRD1.SystemVersion != IRD2.SystemVersion)
printText.AppendLine($"PUP Version: {IRD1.SystemVersion} vs {IRD2.SystemVersion}");
// Print any disc version difference
if (IRD1.DiscVersion != IRD2.DiscVersion)
printText.AppendLine($"Disc Version: {IRD1.DiscVersion} vs {IRD2.DiscVersion}");
// Print any app version difference
if (IRD1.AppVersion != IRD2.AppVersion)
printText.AppendLine($"App Version: {IRD1.AppVersion} vs {IRD2.AppVersion}");
// Un-gzip the headers to compare them
byte[] header1 = Decompress(IRD1.Header);
byte[] header2 = Decompress(IRD2.Header);
// Print the difference in header length, if not 0
if (header1.Length != header2.Length)
printText.AppendLine($"Header Length: {header1.Length} vs {header2.Length}");
if (!header1.SequenceEqual(header2))
printText.AppendLine($"Header: Differs");
// Print number of bytes that the headers differ by, if not 0
int headerDiff;
if (header1.Length < header2.Length)
headerDiff = header2.Length - header1.Length + header1.Where((x, i) => x != header2[i]).Count();
else
headerDiff = header1.Length - header2.Length + header2.Where((x, i) => x != header1[i]).Count();
if (headerDiff != 0)
printText.AppendLine($"Header: Differs by {headerDiff} bytes");
// Un-gzip the footers to compare them
byte[] footer1 = Decompress(IRD1.Footer);
byte[] footer2 = Decompress(IRD2.Footer);
// Print the difference in footer length, if not 0
if (footer1.Length != footer2.Length)
printText.AppendLine($"Footer Length: {footer1.Length} vs {footer2.Length}");
if (!footer1.SequenceEqual(footer2))
printText.AppendLine($"Footer: Differs");
// Print number of bytes that the footers differ by, if not 0
int footerDiff;
if (footer1.Length < footer2.Length)
footerDiff = footer2.Length - footer1.Length + footer1.Where((x, i) => x != footer2[i]).Count();
else
footerDiff = footer1.Length - footer2.Length + footer2.Where((x, i) => x != footer1[i]).Count();
if (footerDiff != 0)
printText.AppendLine($"Footer: Differs by {footerDiff} bytes");
// Print the difference in number of regions, if not 0
if (IRD1.RegionCount != IRD2.RegionCount)
printText.AppendLine($"Region Count: {IRD1.RegionCount} vs {IRD2.RegionCount}");
// Print any differences in region hashes
int regionCount = IRD2.RegionCount < IRD1.RegionCount ? IRD2.RegionCount : IRD1.RegionCount;
if (regionCount > IRD1.RegionHashes.Length)
regionCount = IRD1.RegionHashes.Length;
@@ -527,47 +703,61 @@ namespace IRDKit
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])}");
printText.AppendLine($"Region {i} Hash: {LibIRD.IRD.ByteArrayToHexString(IRD1.RegionHashes[i])} vs {LibIRD.IRD.ByteArrayToHexString(IRD2.RegionHashes[i])}");
}
// Print the difference in number of files, if not 0
if (IRD1.FileCount != IRD2.FileCount)
printText.AppendLine($"File Count: {IRD1.FileCount} vs {IRD2.FileCount}");
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++)
// Print the mismatch file hashes, for each file offset at which they differ
List<long> missingOffsets1 = [];
List<long> missingOffsets2 = [];
for (int i = 0; i < IRD1.FileKeys.Length; i++)
{
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])}");
int j = Array.FindIndex(IRD2.FileKeys, element => element == IRD1.FileKeys[i]);
if (j == -1)
missingOffsets2.Add(IRD1.FileKeys[i]);
if (j != -1 && !IRD1.FileHashes[i].SequenceEqual(IRD2.FileHashes[j]))
printText.AppendLine($"File Hash at Offset {IRD1.FileKeys[i]}: {LibIRD.IRD.ByteArrayToHexString(IRD1.FileHashes[i])} vs {LibIRD.IRD.ByteArrayToHexString(IRD2.FileHashes[j])}");
}
for (int i = 0; i < IRD2.FileKeys.Length; i++)
{
int j = Array.FindIndex(IRD1.FileKeys, element => element == IRD2.FileKeys[i]);
if (j == -1)
missingOffsets1.Add(IRD2.FileKeys[i]);
}
// Print the file offsets that differ
if (missingOffsets1.Count > 0)
printText.AppendLine($"File Offsets not Present in {irdPath1}: {string.Join(", ", missingOffsets1)}");
if (missingOffsets2.Count > 0)
printText.AppendLine($"File Offsets not Present in {irdPath2}: {string.Join(", ", missingOffsets2)}");
// Print any extra config data difference
if (IRD1.ExtraConfig != IRD2.ExtraConfig)
printText.AppendLine($"Extra Config: {IRD1.ExtraConfig:X4} vs {IRD2.ExtraConfig:X4}");
// Print any attachments data difference
if (IRD1.Attachments != IRD2.Attachments)
printText.AppendLine($"Attachments: {IRD1.Attachments:X4} vs {IRD2.Attachments:X4}");
// Print any unique ID difference
if (IRD1.UID != IRD2.UID)
printText.AppendLine($"Unique ID: {IRD1.UID:X8} vs {IRD2.UID:X8}");
// Print any data 1 key difference
if (!IRD1.Data1Key.SequenceEqual(IRD2.Data1Key))
printText.AppendLine($"Data 1 Key: {Convert.ToHexString(IRD1.Data1Key)} vs {Convert.ToHexString(IRD2.Data1Key)}");
printText.AppendLine($"Data 1 Key: {LibIRD.IRD.ByteArrayToHexString(IRD1.Data1Key)} vs {LibIRD.IRD.ByteArrayToHexString(IRD2.Data1Key)}");
// Print any data 2 key difference
if (!IRD1.Data2Key.SequenceEqual(IRD2.Data2Key))
printText.AppendLine($"Data 2 Key: {Convert.ToHexString(IRD1.Data2Key)} vs {Convert.ToHexString(IRD2.Data2Key)}");
printText.AppendLine($"Data 2 Key: {LibIRD.IRD.ByteArrayToHexString(IRD1.Data2Key)} vs {LibIRD.IRD.ByteArrayToHexString(IRD2.Data2Key)}");
// Print any PIC difference
if (!IRD1.PIC.SequenceEqual(IRD2.PIC))
printText.AppendLine($"PIC: {Convert.ToHexString(IRD1.PIC)} vs {Convert.ToHexString(IRD2.PIC)}");
printText.AppendLine($"PIC: {LibIRD.IRD.ByteArrayToHexString(IRD1.PIC)} vs {LibIRD.IRD.ByteArrayToHexString(IRD2.PIC)}");
// Write formatted string to file if output path provided, otherwise to console
if (outPath != null)
File.AppendAllText(outPath, printText.ToString());
else
@@ -583,17 +773,32 @@ namespace IRDKit
/// <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)
public static string ISO2IRD(string isoPath, string irdPath = null, string hexKey = null, string keyPath = null, string getKeyLog = null, long? layerbreak = null, bool verbose = false)
{
// Check file exists
FileInfo iso = new(isoPath);
FileInfo iso;
try
{
iso = new(isoPath);
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
if (!iso.Exists)
{
Console.WriteLine($"{nameof(isoPath)} is not a valid file or directory");
Console.Error.WriteLine($"{nameof(isoPath)} is not a valid file or directory");
return null;
}
// Determine IRD path if only folder given
if (Directory.Exists(irdPath))
irdPath = Path.Combine(irdPath, Path.GetFileName(Path.ChangeExtension(isoPath, ".ird")));
// Determine IRD path if none given
if (irdPath == string.Empty)
irdPath = Path.GetFileName(Path.ChangeExtension(isoPath, ".ird"));
irdPath ??= Path.ChangeExtension(isoPath, ".ird");
// Create new reproducible redump-style IRD with a given hex key
@@ -602,23 +807,22 @@ namespace IRDKit
try
{
// Get disc key from hex string
byte[] discKey = Convert.FromHexString(hexKey);
byte[] discKey = LibIRD.IRD.HexStringToByteArray(hexKey);
if (discKey == null || discKey.Length != 16)
throw new ArgumentException(hexKey);
Console.Error.WriteLine($"{hexKey} is not a valid key, detecting key automatically...");
else
{
Console.WriteLine($"Creating {irdPath} with Key: {hexKey}");
IRD ird1 = new ReIRD(isoPath, discKey, layerbreak);
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
catch (ArgumentException)
{
Console.Error.WriteLine($"{hexKey} is not a valid key, detecting key automatically...");
}
catch (FileNotFoundException)
catch (Exception e)
{
Console.Error.WriteLine("File not found, failed to create IRD");
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
}
@@ -626,23 +830,30 @@ namespace IRDKit
// Create new reproducible redump-style IRD with a given key file
if (keyPath != null)
{
// Read key from .key file
byte[] discKey = File.ReadAllBytes(keyPath);
try
{
IRD ird2 = new ReIRD(isoPath, discKey, layerbreak);
Console.WriteLine($"Creating {irdPath} with Key: {Convert.ToHexString(discKey)}");
ird2.Write(irdPath);
ird2.Print();
// If key directory was given, append filename and .key
if (Directory.Exists(keyPath))
{
keyPath = Path.Combine(keyPath, Path.ChangeExtension(Path.GetFileName(isoPath), ".key"));
}
// Read key from .key file
byte[] discKey = File.ReadAllBytes(keyPath);
if (discKey == null || discKey.Length != 16)
Console.Error.WriteLine($"{hexKey} is not a valid key, detecting key automatically...");
else
{
Console.WriteLine($"Creating {irdPath} with Key: {LibIRD.IRD.ByteArrayToHexString(discKey)}");
IRD ird1 = new ReIRD(isoPath, discKey, layerbreak);
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
catch (ArgumentException)
{
Console.Error.WriteLine($"{Convert.ToHexString(discKey)} is not a valid key, detecting key automatically...");
}
catch (FileNotFoundException)
catch (Exception e)
{
Console.Error.WriteLine("File not found, failed to create IRD");
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
}
@@ -653,14 +864,15 @@ namespace IRDKit
try
{
Console.WriteLine($"Creating {irdPath} with key from: {getKeyLog}");
IRD ird3 = new ReIRD(isoPath, getKeyLog);
ird3.Write(irdPath);
ird3.Print();
IRD ird1 = new ReIRD(isoPath, getKeyLog);
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
catch (FileNotFoundException)
catch (Exception e)
{
Console.Error.WriteLine("File not found, failed to create IRD");
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
}
@@ -676,21 +888,20 @@ namespace IRDKit
// 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();
Console.Error.WriteLine($"{hexKey} is not a valid key, detecting key automatically...");
else
{
Console.WriteLine($"Creating {irdPath} with Key: {LibIRD.IRD.ByteArrayToHexString(discKey)}");
IRD ird1 = new ReIRD(isoPath, discKey, layerbreak);
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
catch (ArgumentException)
{
Console.Error.WriteLine("Given key file not valid, detecting key automatically...");
}
catch (FileNotFoundException)
catch (Exception e)
{
Console.Error.WriteLine("File not found, failed to create IRD");
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
}
@@ -704,88 +915,161 @@ namespace IRDKit
try
{
Console.WriteLine($"Creating {irdPath} with key from: {logfilePath}");
IRD ird3 = new ReIRD(isoPath, logfilePath);
ird3.Write(irdPath);
ird3.Print();
IRD ird1 = new ReIRD(isoPath, logfilePath);
ird1.Write(irdPath);
if (verbose)
ird1.Print();
return irdPath;
}
catch (FileNotFoundException)
catch (Exception e)
{
Console.Error.WriteLine("File not found, failed to create IRD");
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
}
// No key provided, try get key from redump.org
if (verbose)
Console.WriteLine("No key provided... Searching for key on redump.org");
// Compute CRC32 hash
byte[] crc32;
using (FileStream fs = File.OpenRead(isoPath))
{
Crc32 hasher = new();
hasher.Append(fs);
crc32 = hasher.GetCurrentHash();
// Change endianness
byte[] crc32 = HashTool.GetFileHashArray(isoPath, HashType.CRC32);
string crc32String = LibIRD.IRD.ByteArrayToHexString(crc32).ToLowerInvariant();
// Convert to UInt for use as UID in IRD
Array.Reverse(crc32);
}
string crc32_hash = Convert.ToHexString(crc32).ToLower();
uint crc32UInt = BitConverter.ToUInt32(crc32, 0);
// Search for ISO on redump.org
#if !NETFRAMEWORK
RedumpHttpClient redump = new();
List<int> ids = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + crc32_hash).ConfigureAwait(false).GetAwaiter().GetResult();
List<int> ids = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + crc32String).ConfigureAwait(false).GetAwaiter().GetResult();
#else
RedumpWebClient redump = new();
List<int> ids = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + crc32String);
#endif
int id;
if (ids.Count == 0)
{
Console.WriteLine("ISO not found in redump, cannot automatically retreive key");
Console.Error.WriteLine("ISO not found in redump and no valid key provided, cannot create IRD");
return null;
}
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();
// More than one result for the CRC32 hash, compute SHA1 hash instead
string sha1String = HashTool.GetFileHash(isoPath, HashType.SHA1);
// 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 !NETFRAMEWORK
List<int> ids2 = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + sha1String).ConfigureAwait(false).GetAwaiter().GetResult();
#else
List<int> ids2 = redump.CheckSingleSitePage("http://redump.org/discs/system/ps3/quicksearch/" + sha1String);
#endif
if (ids2.Count == 0)
{
Console.WriteLine("ISO not found in redump, cannot automatically retreive key");
Console.Error.WriteLine("ISO not found in redump and no valid key provided, cannot create IRD");
return null;
}
else if (ids2.Count > 1)
{
Console.WriteLine("Cannot automatically get key from redump. Please search redump.org and run again with -k");
Console.Error.WriteLine("Cannot automatically get key from redump. Please search redump.org and run again with -k");
return null;
}
id = ids2[0];
}
else
{
// One result found, assume it is the PS3 ISO
id = ids[0];
}
// Download key file from redump.org
#if !NETFRAMEWORK
byte[] key = redump.GetByteArrayAsync($"http://redump.org/disc/{id}/key").ConfigureAwait(false).GetAwaiter().GetResult();
#else
byte[] key = redump.DownloadData($"http://redump.org/disc/{id}/key");
#endif
if (key.Length != 16)
{
Console.WriteLine("Invalid key obtained from redump");
Console.Error.WriteLine("Invalid key obtained from redump and no valid key provided, cannot create IRD");
return null;
}
// Create IRD with key from redump
Console.WriteLine($"Creating {irdPath} with Key: {Convert.ToHexString(key)}");
IRD ird = new ReIRD(isoPath, key, layerbreak);
Console.WriteLine($"Creating {irdPath} with Key from redump.org: {LibIRD.IRD.ByteArrayToHexString(key)}");
try
{
IRD ird = new ReIRD(isoPath, key, layerbreak, crc32UInt);
ird.Write(irdPath);
if (verbose)
ird.Print();
return irdPath;
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message + ", failed to create IRD");
return null;
}
}
#endregion
public static XDocument DatParser(string datpath = null)
{
try
{
if (!File.Exists(datpath))
return null;
return XDocument.Load(datpath);
}
catch
{
return null;
}
}
public static string GetDatFilename(IRD ird, XDocument datfile)
{
string crc32 = ird.UID.ToString("X8").ToLower();
XElement node = datfile.Root.Elements("game").Where(e => e.Element("rom").Attribute("crc").Value == crc32).FirstOrDefault() ?? throw new ArgumentException("Cannot find ISO in redump DAT");
return node.Attribute("name").Value;
}
public static void RenameIRD(string irdPath, XDocument datfile, bool serial = false, bool crc = false, bool verbose = false)
{
IRD ird = IRD.Read(irdPath);
if (ird.ExtraConfig != 0x0001)
throw new ArgumentException($"{irdPath} is not a redump-style IRD");
string filename = GetDatFilename(ird, datfile) ?? throw new ArgumentException($"Cannot determine DAT filename for {irdPath}");
if (serial)
filename += $" [{ird.TitleID.Substring(0, 4)}-{ird.TitleID.Substring(5, 5)}]";
if (crc)
filename += $" [{ird.UID:X8}]";
// Rename irdPath to filename
string directory = Path.GetDirectoryName(Path.GetFullPath(filename));
string filepath;
if (!string.IsNullOrEmpty(directory))
filepath = Path.Combine(Path.GetDirectoryName(irdPath), filename + ".ird");
else
filepath = filename + ".ird";
// Rename IRD to new name
if (irdPath != filepath)
{
if (verbose)
Console.WriteLine($"Renaming {Path.GetFileName(irdPath)} to {Path.GetFileName(filepath)}");
File.Move(irdPath, filepath);
}
else
{
if (verbose)
Console.WriteLine($"Skipping {Path.GetFileName(irdPath)}, already named correctly");
}
}
#endregion
#region Helper Functions
+40 -6
View File
@@ -1,7 +1,41 @@
## 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 game.iso game.ird
```
For detailed usage, run `irdkit --help`
IRDKit is a tool that allows direct use of LibIRD functionality from the command line interface.
For full help instructions, run `irdkit help`
## Creating ISOs
For all options, run `irdkit help create`
To create an IRD from an ISO, run `irdkit create game.iso`
Multiple ISOs can be processed at once with `irdkit create game1.iso game2.iso`
Or a whole directory of ISOs can be processed with `irdkit create ./ISO`, or recursively with `irdkit create -r ./ISO`
The IRD will be created in the same folder as the ISO, with the same filename.
A different IRD path and/or filename can be defined with `-o` or `--output=`
### Key
By default, IRDs will be created by pulling keys from redump.org
A key can be manually provided with `-k` or `--key=`
A key file can be provided with `-f game.key` or `--key-file=`
A key from GetKey log file can be used with `-l game.getkey.log` or `--getkey-log=`
### PIC
By default, a PIC will be generated assuming a default layerbreak
A layerbreak value can be provided with `-b` or `--layerbreak=`
A PIC from a GetKey log file can be used with `-l game.getkey.log` or `--getkey-log=`
## Printing info
For all options, run `irdkit help info`
To print info about an ISO or IRD file, run `irdkit info game.iso` or `irdkit info game.ird`
The info can be printed to a file, e.g. `-o out.txt` or `--output=`
The info can be formatted as a JSON with `-j` or `--json`
## Comparing IRDs
For all options, run `irdkit help diff`
To compare two IRDs, run `irdkit diff game1.ird game2.ird`
The comparison can be printed to a file, e.g. `-o out.txt` or `--output=`
+2 -18
View File
@@ -11,16 +11,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
.gitmodules = .gitmodules
LICENSE.txt = LICENSE.txt
publish-irdkit.ps1 = publish-irdkit.ps1
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils.Core", "DiscUtils\Library\DiscUtils.Core\DiscUtils.Core.csproj", "{9E71F5CD-887D-4A61-8460-9F7E8F110B46}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils.Iso9660", "DiscUtils\Library\DiscUtils.Iso9660\DiscUtils.Iso9660.csproj", "{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils.Streams", "DiscUtils\Library\DiscUtils.Streams\DiscUtils.Streams.csproj", "{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -35,18 +31,6 @@ Global
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9060E3AF-E4FB-436F-93A1-5C47389CB88E}.Release|Any CPU.Build.0 = Release|Any CPU
{9E71F5CD-887D-4A61-8460-9F7E8F110B46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E71F5CD-887D-4A61-8460-9F7E8F110B46}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E71F5CD-887D-4A61-8460-9F7E8F110B46}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E71F5CD-887D-4A61-8460-9F7E8F110B46}.Release|Any CPU.Build.0 = Release|Any CPU
{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E1EF05F-388B-42C6-A4E7-98BE20C7C8EC}.Release|Any CPU.Build.0 = Release|Any CPU
{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8D0B8B0-F165-4FA1-AC7B-194F5F48A5F9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+1
Submodule LibIRD/DiscUtils added at 445a572137
+437 -196
View File
@@ -1,12 +1,13 @@
using DiscUtils;
using DiscUtils.Iso9660;
using DiscUtils.Streams;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Hashing;
using System.Security.Cryptography;
using System.Text;
using LibIRD.DiscUtils;
using LibIRD.DiscUtils.Iso9660;
using LibIRD.DiscUtils.Streams;
using SabreTools.Hashing;
namespace LibIRD
{
@@ -117,7 +118,6 @@ namespace LibIRD
}
}
private byte[] _discKey;
// TODO: Link Data1Key and Disc Key
/// <summary>
/// D1 key
@@ -271,7 +271,7 @@ namespace LibIRD
/// <summary>
/// MD5 hashes for all decrypted files in the image
/// </summary>
/// <remarks><see cref="FileHashes"/> files, 16-bytes per hash, alternating with each <see cref="FileHashes"/> entry</remarks>
/// <remarks><see cref="FileCount"/> files, 16-bytes per hash, alternating with each <see cref="FileHashes"/> entry</remarks>
public byte[][] FileHashes { get; private set; }
/// <summary>
@@ -301,6 +301,11 @@ namespace LibIRD
/// </summary>
private long[] RegionEnd { get; set; }
/// <summary>
/// File extents to hash
/// </summary>
private Range<long, long>[][] FileExtents { get; set; }
#endregion
#region Constructors
@@ -352,11 +357,9 @@ namespace LibIRD
/// <summary>
/// Default constructor for internal derived classes only: resulting object not in usable state
/// Assumes that internally derived class will set fields in its own constructor
/// </summary>
private protected IRD()
{
// Assumes that internally derived class will set fields in its own constructor
}
private protected IRD() { }
/// <summary>
/// Constructor with given required fields
@@ -406,7 +409,6 @@ namespace LibIRD
private protected static byte[] GenerateD1(byte[] key)
{
// Validate 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));
@@ -440,7 +442,6 @@ namespace LibIRD
private protected static byte[] GenerateDiscKey(byte[] d1)
{
// Validate key
ArgumentNullException.ThrowIfNull(d1, nameof(d1));
if (d1.Length != 16)
throw new ArgumentException("Disc Key must be a byte array of length 16", nameof(d1));
@@ -474,7 +475,6 @@ namespace LibIRD
private protected static byte[] GenerateD2(byte[] d2)
{
// Validate id
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
@@ -507,7 +507,6 @@ namespace LibIRD
private protected static byte[] GenerateDiscID(byte[] d2)
{
// Validate id
ArgumentNullException.ThrowIfNull(d2 , nameof(d2));
if (d2.Length != 16)
throw new ArgumentException("Disc ID must be a byte array of length 16", nameof(d2));
@@ -540,14 +539,12 @@ namespace LibIRD
/// <exception cref="InvalidDataException"></exception>
private protected void ParseGetKeyLog(string getKeyLog)
{
// Validate .getkey.log file path
ArgumentNullException.ThrowIfNull(getKeyLog, nameof(getKeyLog));
if (!File.Exists(getKeyLog))
if (!System.IO.File.Exists(getKeyLog))
throw new FileNotFoundException(nameof(getKeyLog));
// Read from .getkey.log file
using StreamReader sr = File.OpenText(getKeyLog);
using StreamReader sr = System.IO.File.OpenText(getKeyLog);
// Determine whether GetKey was successful
string line;
@@ -561,12 +558,12 @@ namespace LibIRD
if (line == null)
throw new InvalidDataException("Could not find Disc Key in .getkey.log");
// Get Disc Key from log
string discKeyStr = line["disc_key = ".Length..];
string discKeyStr = line.Substring("disc_key = ".Length);
// Validate Disc Key from log
if (discKeyStr.Length != 32)
throw new InvalidDataException("Unexpected Disc Key in .getkey.log");
// Convert Disc Key to byte array
discKey = Convert.FromHexString(discKeyStr);
discKey = HexStringToByteArray(discKeyStr);
// Read Disc ID
byte[] discID;
@@ -574,14 +571,14 @@ namespace LibIRD
if (line == null)
throw new InvalidDataException("Could not find Disc ID in .getkey.log");
// Get Disc ID from log
string discIDStr = line["disc_id = ".Length..];
string discIDStr = line.Substring("disc_id = ".Length);
// Validate Disc ID from log
if (discIDStr.Length != 32)
throw new InvalidDataException("Unexpected Disc ID in .getkey.log");
// Replace X's in Disc ID with 00000001
discIDStr = discIDStr[..24] + "00000001";
discIDStr = discIDStr.Substring(0, 24) + "00000001";
// Convert Disc ID to byte array
discID = Convert.FromHexString(discIDStr);
discID = HexStringToByteArray(discIDStr);
// Look for PIC in log
byte[] discPIC;
@@ -596,7 +593,7 @@ namespace LibIRD
if (discPICStr.Length != 256)
throw new InvalidDataException("Unexpected PIC in .getkey.log");
// Convert PIC to byte array
discPIC = Convert.FromHexString(discPICStr[..230]);
discPIC = HexStringToByteArray(discPICStr.Substring(0, 230));
// Double check for warnings in .getkey.log
while ((line = sr.ReadLine()) != null && line.Trim().StartsWith("WARNING") == false && line.Trim().StartsWith("SUCCESS") == false)
@@ -621,17 +618,17 @@ namespace LibIRD
/// <param name="redump">True if redump-style IRD</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="FileNotFoundException"></exception>
/// <exception cref="InvalidFileSystemException"></exception>
/// <exception cref="IOException"></exception>
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);
using FileStream fs = new FileStream(isoPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan) ?? throw new FileNotFoundException(isoPath);
// Validate ISO file stream
if (!CDReader.Detect(fs))
throw new InvalidFileSystemException("Not a valid ISO file");
throw new IOException("Not a valid ISO file");
// New ISO Reader from DiscUtils
CDReader reader = new(fs, true, true);
CDReader reader = new(fs);
// If generating redump-style IRD
if (redump)
@@ -640,7 +637,7 @@ namespace LibIRD
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);
using SparseStream s = reader.OpenFile("PS3_DISC.SFB", FileMode.Open, FileAccess.Read);
// Parse PS3_DISC.SFB file
PS3_DiscSFB ps3_DiscSFB = new(s);
@@ -648,7 +645,7 @@ namespace LibIRD
bool titleIDFound = ps3_DiscSFB.Field.TryGetValue("TITLE_ID", out string titleID);
// If a valid TITLE_ID field is present, remove the hyphen to fit into standard IRD file
if (titleIDFound && titleID.Length == 10 && titleID[4] == '-')
TitleID = string.Concat(titleID.AsSpan(0, 4), titleID.AsSpan(5, 5));
TitleID = titleID.Substring(0, 4) + titleID.Substring(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
@@ -658,7 +655,7 @@ namespace LibIRD
}
// Read PS3 Metadata from PARAM.SFO
using (DiscUtils.Streams.SparseStream s = reader.OpenFile("\\PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read))
using (SparseStream s = reader.OpenFile("\\PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read))
{
// Parse PARAM.SFO file
ParamSFO paramSFO = new(s);
@@ -668,7 +665,7 @@ namespace LibIRD
{
bool titleIDFound = paramSFO.Field.TryGetValue("TITLE_ID", out string titleID);
if (titleIDFound)
TitleID = titleID.Length == 9 ? titleID : titleID.PadRight(9, '\0')[..9];
TitleID = titleID.Length == 9 ? titleID : titleID.PadRight(9, '\0').Substring(0, 9);
else
TitleID = "\0\0\0\0\0\0\0\0\0";
}
@@ -682,7 +679,7 @@ namespace LibIRD
{
bool discVersionFound = paramSFO.Field.TryGetValue("VERSION", out string discVersion);
if (discVersionFound)
DiscVersion = discVersion.Length == 5 ? discVersion : discVersion.PadRight(5, '\0')[..5];
DiscVersion = discVersion.Length == 5 ? discVersion : discVersion.PadRight(5, '\0').Substring(0, 5);
else
DiscVersion = "\0\0\0\0\0";
}
@@ -690,7 +687,7 @@ namespace LibIRD
// 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];
AppVersion = appVersion.Length == 5 ? appVersion : appVersion.PadRight(5, '\0').Substring(0, 5);
else
AppVersion = "\0\0\0\0\0";
}
@@ -704,30 +701,40 @@ namespace LibIRD
// Read and compress the ISO footer
GetFooter(fs);
// Process all regions on ISO
HashRegions(fs);
// TODO: Speed up program by hashing regions and files at the same time (read from filesystem only once)
// Get info region info from ISO
GetRegions(fs);
// Recursively count all files in ISO to allocate file arrays
DiscDirectoryInfo rootDir = reader.GetDirectoryInfo("\\");
FileCount = 0;
CountFiles(rootDir);
FileKeys = new long[FileCount];
FileHashes = new byte[FileCount][];
// Determine file offsets and hashes
// Pre-allocate arrays and reset file count
FileKeys = new long[FileCount];
FileExtents = new Range<long, long>[FileCount][];
uint fileCount = FileCount;
FileCount = 0;
HashFiles(fs, reader, rootDir);
// Determine file offsets
GetFiles(fs, reader, rootDir);
// Resize arrays if non-contiguous files were detected
if (FileCount != fileCount)
{
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;
Range<long, long>[][] tempFileExtents = FileExtents;
Array.Resize(ref tempFileExtents, (int)FileCount);
FileExtents = tempFileExtents;
}
Array.Sort(FileKeys, FileHashes);
// Sort files by offset
Array.Sort(FileKeys, FileExtents);
// Calculate CRC32 hash of ISO only if generating a redump IRD and the UID is not already set
RegionHashes = new byte[RegionCount][];
FileHashes = new byte[FileCount][];
HashISO(fs, redump && UID == 0x00000000);
}
#endregion
@@ -739,19 +746,20 @@ namespace LibIRD
/// </summary>
/// <remarks>PS3UPDAT.PUP update file version number</remarks>
/// <param name="fs">ISO filestream</param>
/// <param name="reader"></param>
/// <exception cref="InvalidFileSystemException"></exception>
/// <param name="reader">CDReader</param>
/// <exception cref="IOException"></exception>
private void GetSystemVersion(FileStream fs, CDReader reader)
{
// Determine PUP file offset via cluster
DiscUtils.Streams.Range<long, long>[] updateClusters = reader.PathToClusters("\\PS3_UPDATE\\PS3UPDAT.PUP");
Range<long, long>[] updateClusters = reader.PathToClusters("\\PS3_UPDATE\\PS3UPDAT.PUP");
if (updateClusters == null && updateClusters.Length == 0 && updateClusters[0] == null)
throw new InvalidFileSystemException("Invalid file extents for PS3UPDAT.PUP");
throw new IOException("Invalid file extents for PS3UPDAT.PUP");
// PS3UPDAT.PUP file begins at first byte of dedicated cluster
UpdateOffset = SectorSize * updateClusters[0].Offset;
// Update file ends at the last byte of the last cluster
UpdateEnd = SectorSize * updateClusters[^1].Offset + updateClusters[^1].Count;
int lastExtent = updateClusters.Length - 1;
UpdateEnd = SectorSize * updateClusters[lastExtent].Offset + updateClusters[lastExtent].Count;
// Check PUP file Magic
fs.Seek(UpdateOffset, SeekOrigin.Begin);
@@ -782,20 +790,26 @@ namespace LibIRD
/// Retreives and stores the header
/// </summary>
/// <param name="fs">ISO filestream</param>
/// <param name="reader"></param>
/// <exception cref="InvalidFileSystemException"></exception>
/// <param name="reader">CDReader</param>
/// <exception cref="IOException"></exception>
private void GetHeader(FileStream fs, CDReader reader)
{
// Determine the extent of the header via cluster (Sector 0 to first data sector)
DiscUtils.Streams.Range<long, long>[] sfbClusters = reader.PathToClusters("\\PS3_DISC.SFB");
Range<long, long>[] sfbClusters = reader.PathToClusters("\\PS3_DISC.SFB");
if (sfbClusters == null && sfbClusters.Length == 0 && sfbClusters[0] == null)
throw new InvalidFileSystemException("Invalid file extents for PS3_DISC.SFB");
throw new IOException("Invalid file extents for PS3_DISC.SFB");
// End of header is at beginning of first byte of dedicated cluster
FirstDataSector = sfbClusters[0].Offset;
// Begin a GZip stream to write header to
using MemoryStream headerStream = new();
using (GZipStream gzs = new(headerStream, CompressionLevel.SmallestSize))
#if NET6_0_OR_GREATER
using (GZipStream gzStream = new(headerStream, CompressionLevel.SmallestSize))
#elif NETCOREAPP || NET45_OR_GREATER
using (GZipStream gzStream = new(headerStream, CompressionLevel.Optimal))
#else
using (GZipStream gzStream = new(headerStream, CompressionMode.Compress))
#endif
{
// Start reading data from the beginning of the ISO file
fs.Seek(0, SeekOrigin.Begin);
@@ -806,7 +820,7 @@ namespace LibIRD
for (int i = 0; i < FirstDataSector; i++)
{
numBytes = fs.Read(buf, 0, buf.Length);
gzs.Write(buf, 0, numBytes);
gzStream.Write(buf, 0, numBytes);
}
}
@@ -823,7 +837,13 @@ namespace LibIRD
{
// Begin a GZip stream to write footer to
using MemoryStream footerStream = new();
using (GZipStream gzs = new(footerStream, CompressionLevel.SmallestSize))
#if NET6_0_OR_GREATER
using (GZipStream gzStream = new(footerStream, CompressionLevel.SmallestSize))
#elif NETCOREAPP || NET45_OR_GREATER
using (GZipStream gzStream = new(footerStream, CompressionLevel.Optimal))
#else
using (GZipStream gzStream = new(footerStream, CompressionMode.Compress))
#endif
{
// Start reading data from after last file (PS3UPDAT.PUP)
fs.Seek(UpdateEnd, SeekOrigin.Begin);
@@ -834,7 +854,7 @@ namespace LibIRD
while (numBytes != 0)
{
numBytes = fs.Read(buf, 0, buf.Length);
gzs.Write(buf, 0, numBytes);
gzStream.Write(buf, 0, numBytes);
}
}
@@ -844,11 +864,11 @@ namespace LibIRD
}
/// <summary>
/// Determines and stores the hashes for each disc region
/// Retreives and stores the Region extents
/// </summary>
/// <param name="fs"></param>
/// <exception cref="InvalidFileSystemException"></exception>
private void HashRegions(FileStream fs)
/// <param name="fs">ISO filestream</param>
/// <exception cref="IOException"></exception>
private void GetRegions(FileStream fs)
{
// Determine the number of unencryted regions
fs.Seek(0, SeekOrigin.Begin);
@@ -858,8 +878,7 @@ namespace LibIRD
// Total number of regions is 2x number of unencrypted regions, minus 1
RegionCount = (byte)(2 * ((uint)decRegionCount[3]) - 1);
if (RegionCount <= 0)
throw new InvalidFileSystemException("No regions detected in ISO");
RegionHashes = new byte[RegionCount][];
throw new IOException("No regions detected in ISO");
RegionStart = new long[RegionCount];
RegionEnd = new long[RegionCount];
@@ -872,56 +891,32 @@ namespace LibIRD
{
// End sector of previous region is start of this region
if (i % 2 == 1)
RegionStart[i] = BitConverter.ToInt32(regionSector) + 1;
RegionStart[i] = BitConverter.ToInt32(regionSector, 0) + 1;
else
RegionStart[i] = BitConverter.ToInt32(regionSector);
RegionStart[i] = BitConverter.ToInt32(regionSector, 0);
// Determine end sector offset of this region
fs.Read(regionSector, 0, 4);
Array.Reverse(regionSector, 0, 4);
if (i % 2 == 1)
RegionEnd[i] = BitConverter.ToInt32(regionSector) - 1;
RegionEnd[i] = BitConverter.ToInt32(regionSector, 0) - 1;
else
RegionEnd[i] = BitConverter.ToInt32(regionSector);
RegionEnd[i] = BitConverter.ToInt32(regionSector, 0);
}
// Remove header from first region
RegionStart[0] = FirstDataSector;
// Remove footer from last region
RegionEnd[^1] = (UpdateEnd / SectorSize) - 1;
// Determine MD5 hashes for each region
using MD5 md5 = MD5.Create();
byte[] buf = new byte[SectorSize];
for (int i = 0; i < RegionCount; i++)
{
// Start reading data from first sector of region
fs.Seek(SectorSize * RegionStart[i], SeekOrigin.Begin);
// Compute MD5 hash for just the region portion of the ISO file
int numBytes;
for (long j = RegionStart[i]; j <= RegionEnd[i]; j++)
{
// Read one sector at a time
numBytes = fs.Read(buf, 0, buf.Length);
// Check that an entire sector was read
if (numBytes < buf.Length)
throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Process MD5 sum one sector at a time
md5.TransformBlock(buf, 0, buf.Length, null, 0);
}
// Compute and store MD5 hash of region
md5.TransformFinalBlock(buf, 0, 0);
RegionHashes[i] = md5.Hash;
}
int lastRegion = RegionEnd.Length - 1;
RegionEnd[lastRegion] = (UpdateEnd / SectorSize) - 1;
}
/// <summary>
/// Determine and store hashes for all files and files within subdirectories recursively
/// Determine and store file extents for all files and files within subdirectories recursively
/// </summary>
/// <param name="reader"></param>
/// <param name="path"></param>
private void HashFiles(FileStream fs, CDReader reader, DiscDirectoryInfo dir)
/// <param name="fs">ISO filestream</param>
/// <param name="reader">CDReader</param>
/// <param name="dir">Folder to search for files within</param>
private void GetFiles(FileStream fs, CDReader reader, DiscDirectoryInfo dir)
{
// Process all files in current directory
foreach (DiscFileInfo fileInfo in dir.GetFiles())
@@ -929,124 +924,104 @@ namespace LibIRD
string filePath = fileInfo.FullName;
// Determine the extents of the file via clusters
DiscUtils.Streams.Range<long, long>[] fileClusters = reader.PathToClusters(filePath);
Range<long, long>[] fileExtent = reader.PathToClusters(filePath);
// If invalid clusters were returned, we can't hash this file
if (fileClusters == null && fileClusters.Length == 0)
throw new InvalidFileSystemException($"Unexpected file extents for {filePath}");
if (fileExtent == null && fileExtent.Length == 0)
throw new IOException($"Unexpected file extents for {filePath}");
// Determine smallest file offset as first sector
long smallestOffset = fileClusters[0].Offset;
for (int i = 1; i < fileClusters.Length; i++)
long smallestOffset = fileExtent[0].Offset;
bool nonContiguous = false;
for (int i = 1; i < fileExtent.Length; i++)
{
if (fileClusters[i] == null)
throw new InvalidFileSystemException($"Unexpected file extents for {filePath}");
if (fileExtent[i] == null)
throw new IOException($"Unexpected file extents for {filePath}");
if (fileClusters[i].Offset < smallestOffset)
smallestOffset = fileClusters[i].Offset;
if (fileExtent[i].Offset * SectorSize != fileExtent[i - 1].Offset * SectorSize + fileExtent[i - 1].Count)
nonContiguous = true;
if (fileExtent[i].Offset < smallestOffset)
smallestOffset = fileExtent[i].Offset;
}
int firstSector = (int)(smallestOffset);
// If already encountered file offset, skip this file
if (Array.Exists(FileKeys, element => element == firstSector))
if (Array.Exists(FileKeys, element => element == smallestOffset))
continue;
else if (nonContiguous)
Console.WriteLine($"Non-contiguous file found: {filePath}");
// Add file offset to keys
FileKeys[FileCount] = firstSector;
// Determine whether file is in encrypted or decrypted region
bool encrypted = false;
for (int i = RegionCount - 1; i > 0; i--)
{
if (RegionStart[i] <= firstSector)
{
encrypted = i % 2 == 1;
break;
}
}
// Hash each non-contiguous portion of the ISO file
byte[] buf = new byte[SectorSize];
MD5 md5 = MD5.Create();
for (int i = 0; i < fileClusters.Length; i++)
{
// Start reading data from the beginning of the ISO file
fs.Seek(fileClusters[i].Offset * SectorSize, SeekOrigin.Begin);
int numBytes;
// Read all data before the first data sector
for (int j = 0; j < (fileClusters[i].Count / SectorSize); j++)
{
numBytes = fs.Read(buf, 0, buf.Length);
// Check that an entire sector was read
if (numBytes < buf.Length)
throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Decrypt sector if necessary
if (encrypted)
buf = DecryptSector(buf, firstSector + j);
// Hash sector
md5.TransformBlock(buf, 0, numBytes, null, 0);
}
// Read remaining partial sector
if (fileClusters[i].Count % SectorSize != 0)
{
numBytes = fs.Read(buf, 0, buf.Length);
// Check that an entire sector was read
if (numBytes < buf.Length)
throw new InvalidFileSystemException("Disc region ended unexpectedly");
// Decrypt partial sector if necessary
if (encrypted)
buf = DecryptSector(buf, 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
md5.TransformFinalBlock(buf, 0, 0);
FileHashes[FileCount] = md5.Hash;
// Add file offset to keys and extents to extents
FileKeys[FileCount] = smallestOffset;
FileExtents[FileCount] = fileExtent;
FileCount++;
}
// Recursively process all subfolders of current directory
foreach (DiscDirectoryInfo dirInfo in dir.GetDirectories())
{
HashFiles(fs, reader, dirInfo);
GetFiles(fs, reader, dirInfo);
}
}
/// <summary>
/// Decrypts a given sector byte array
/// Decrypts a given byte array of sector(s)
/// </summary>
/// <param name="sector">Byte array to be decrypted</param>
/// <param name="buffer">Byte array to be decrypted</param>
/// <param name="sectorNumber">Sector number of first sector being decrypted</param>
/// <param name="offset">Number of sectors to skip</param>
/// <param name="count">Number of sectors to decrypt, beginning from offset</param>
/// <exception cref="InvalidOperationException"></exception>
private protected byte[] DecryptSector(byte[] sector, int sectorNumber)
private protected void DecryptSectors(ref byte[] buffer, int sectorNumber, int offset = 0, int? count = null)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (buffer.Length == 0 || buffer.Length % SectorSize != 0)
throw new ArgumentException("Encrypted buffer must be multiple of SectorSize");
if (offset < 0 || offset >= buffer.Length / SectorSize)
throw new ArgumentException("Offset sector must be within buffer");
count ??= (int)(buffer.Length / SectorSize) - offset;
if (count < 0 || count > (buffer.Length / SectorSize) - offset)
throw new ArgumentException("Number of sectors must be within buffer");
// Setup AES decryption
using Aes aes = Aes.Create() ?? throw new InvalidOperationException("AES not available. Change your system settings");
// Set AES settings
aes.Key = DiscKey;
aes.Padding = PaddingMode.None;
aes.Mode = CipherMode.CBC;
// Determine Initial Value based on sector number
byte[] iv = new byte[16];
for (int i = 0; i < 16; i++)
// Convert offset and count to number of bytes
offset *= (int)SectorSize;
count *= (int)SectorSize;
// Decrypt buffer one sector at a time
for (int i = offset; i < offset + count; i += (int)SectorSize)
{
byte a = (byte)(sectorNumber & 0xFF);
iv[16 - i - 1] = (byte)(sectorNumber & 0xFF);
sectorNumber >>= 8;
// Determine AES Initial Value based on sector number
byte[] iv = new byte[16];
int tempNum = sectorNumber;
for (int j = 0; j < 16; j++)
{
iv[16 - j - 1] = (byte)(tempNum & 0xFF);
tempNum >>= 8;
}
aes.IV = iv;
// Perform AES decryption
using MemoryStream stream = new();
using ICryptoTransform dec = aes.CreateDecryptor();
using CryptoStream cs = new(stream, dec, CryptoStreamMode.Write);
cs.Write(sector, 0, sector.Length);
using CryptoStream cs = new(stream, aes.CreateDecryptor(), CryptoStreamMode.Write);
cs.Write(buffer, i, (int)SectorSize);
cs.FlushFinalBlock();
return stream.ToArray();
// Write decrypted sector to output
stream.ToArray().CopyTo(buffer, i);
sectorNumber++;
}
return;
}
/// <summary>
@@ -1060,6 +1035,203 @@ namespace LibIRD
CountFiles(dirInfo);
}
/// <summary>
/// Calculate hashes for all region and file extents
/// </summary>
/// <param name="fs">ISO filestream</param>
/// <param name="redump">True if also calculating CRC32 of entire ISO</param>
private void HashISO(FileStream fs, bool redump)
{
// Initialise CRC32 ISO hasher, only used if making redump-style IRD
byte[] crc32;
HashWrapper isoHasher = new(HashType.CRC32);
// Initialise MD5 region hashes
List<int> regions = [];
HashWrapper[] regionMD5 = new HashWrapper[RegionCount];
for (int i = 0; i < RegionCount; i++)
{
regions.Add(i);
regionMD5[i] = new(HashType.MD5);
}
// Initialise MD5 file hashes
List<int> files = [];
HashWrapper[] fileMD5 = new HashWrapper[FileCount];
for (int i = 0; i < FileCount; i++)
{
files.Add(i);
fileMD5[i] = new(HashType.MD5);
}
// Start hashing from beginning of ISO
long currentSector = 0;
fs.Seek(currentSector, SeekOrigin.Begin);
// Read from ISO, 1024 sectors at a time
int bufSectors = 1024;
byte[] buf = new byte[bufSectors * SectorSize];
while (true)
{
// Attempt to read a full buffer
bufSectors = 1024;
int numBytes = fs.Read(buf, 0, buf.Length);
// If end of ISO reached, stop reading
if (numBytes == 0)
{
// If making redump-style IRD, save CRC32 hash to UID field
if (redump)
{
crc32 = isoHasher.CurrentHashBytes;
Array.Reverse(crc32);
UID = BitConverter.ToUInt32(crc32, 0);
}
return;
}
// Keep trying to read to fill buffer, remove once partial buffer hashing is supported
if (numBytes != buf.Length)
{
while (numBytes % SectorSize != 0)
{
int newNumBytes = fs.Read(buf, numBytes, buf.Length - numBytes);
numBytes += newNumBytes;
// If end of ISO reached, trim buffer and hash
if (newNumBytes == 0 && numBytes % SectorSize != 0)
{
//numBytes -= numBytes % (int)SectorSize;
Console.Error.WriteLine("ERROR: ISO filestream ended early");
break;
}
}
// Only hash portion of buffer
bufSectors = numBytes / (int)SectorSize;
if (bufSectors == 0)
Console.Error.WriteLine("ERROR: Trailing partial sector in ISO filestream");
if (numBytes > buf.Length)
throw new IOException("ERROR: Read more bytes than buffer size???");
}
// Hash ISO
if (redump)
isoHasher.Process(buf, 0, numBytes);
// Hash regions
List<int> regionsEnded = [];
foreach (int i in regions)
{
// Stop hashing regions if current region has not yet started (assumes regions are ordered)
if (RegionStart[i] > currentSector + bufSectors)
break;
// Skip region if it has already ended
//if (RegionEnd[i] < currentSector)
// continue;
// Check if region has ended in this buffer [We know: Start is not in the future, Ending is not in the past]
if (RegionEnd[i] < currentSector + bufSectors)
{
// Determine start byte, if region is entirely within the buffer
int startByte = RegionStart[i] > currentSector ? (int)(SectorSize * (RegionStart[i] - currentSector)) : 0;
// Determine end byte
int endByte = (int)(SectorSize * (RegionEnd[i] - currentSector + 1));
// Close region hash
regionMD5[i].Process(buf, startByte, endByte - startByte);
regionMD5[i].Terminate();
RegionHashes[i] = regionMD5[i].CurrentHashBytes;
regionMD5[i].Dispose();
regionsEnded.Add(i);
}
// Check if region has already begun
else if (RegionStart[i] <= currentSector)
{
// Hash buffer
regionMD5[i].Process(buf, 0, (int)SectorSize * bufSectors);
}
// Region Start is in this buffer, ending is in the future
else
{
// Hash partial buffer
int regionStart = (int)(SectorSize * (RegionStart[i] - currentSector));
regionMD5[i].Process(buf, regionStart, (int)SectorSize * bufSectors - regionStart);
}
}
if (regionsEnded.Count > 0)
regions.RemoveAll(item => regionsEnded.Contains(item));
// Decrypt any encrypted sectors of buffer
for (int i = 1; i < RegionCount; i += 2)
{
// If the current encrypted region is within the buffer
if (RegionStart[i] < currentSector + bufSectors
&& RegionEnd[i] >= currentSector)
{
// First sector to decrypt from
int encOffset = 0;
// Don't decrypt initial sectors if the encrypted region starts within this buffer
if (RegionStart[i] > currentSector)
encOffset = (int)(RegionStart[i] - currentSector);
// Number of sectors to decrypt
int encCount = bufSectors - encOffset;
// Don't decrypt last sectors if the encrypted region ends within this buffer
if (RegionEnd[i] < currentSector + bufSectors)
encCount -= (int)(currentSector + bufSectors - RegionEnd[i] + 1);
// Decrypt encrypted sectors
DecryptSectors(ref buf, (int)currentSector + encOffset, encOffset, encCount);
}
}
// Hash files
List<int> filesEnded = [];
foreach (int i in files)
{
// Stop hashing files if current file has not yet started (assumes FileKeys are sorted)
if (FileKeys[i] > currentSector + bufSectors)
break;
// Hash each file extent for each file
for (int j = 0; j < FileExtents[i].Length; j++)
{
// Skip hashing file extent if it has not yet started or already ended
if (FileExtents[i][j].Offset > currentSector + bufSectors
|| SectorSize * FileExtents[i][j].Offset + FileExtents[i][j].Count < SectorSize * currentSector)
continue;
// Determine first file byte location in buffer
int startByte = FileExtents[i][j].Offset > currentSector ? (int)(SectorSize * (FileExtents[i][j].Offset - currentSector)) : 0;
// Determine last file byte location in buffer
int endByte = (int)(FileExtents[i][j].Count - SectorSize * (currentSector - FileExtents[i][j].Offset));
// Don't hash more than the buffer size
endByte = endByte < bufSectors * (int)SectorSize ? endByte : bufSectors * (int)SectorSize;
// Hash portion of buffer that file exists in
fileMD5[i].Process(buf, startByte, endByte - startByte);
}
// Check if current file has ended in this buffer (assumes last extent contains last byte)
int lastExtent = FileExtents[i].Length - 1;
long lastByte = SectorSize * FileExtents[i][lastExtent].Offset + FileExtents[i][lastExtent].Count;
if (lastByte < SectorSize * (currentSector + bufSectors)
&& lastByte > SectorSize * currentSector)
{
// Close file hash
fileMD5[i].Terminate();
FileHashes[i] = fileMD5[i].CurrentHashBytes;
fileMD5[i].Dispose();
filesEnded.Add(i);
}
}
if (filesEnded.Count > 0)
files.RemoveAll(item => filesEnded.Contains(item));
currentSector += bufSectors;
}
}
#endregion
#region IRD File
@@ -1078,9 +1250,9 @@ namespace LibIRD
// Create new stream to uncompressed IRD contents
using MemoryStream stream = new();
// Write IRD data to stream in order
using (BinaryWriter bw = new(stream, Encoding.UTF8, true))
{
// Write IRD data to stream as UTF8
using BinaryWriter bw = new(stream, Encoding.UTF8);
// IRD File Signature
bw.Write(Magic);
@@ -1127,7 +1299,12 @@ namespace LibIRD
// Hashes for each region
for (int i = 0; i < RegionCount; i++)
{
if (RegionHashes[i] == null)
bw.Write(NullMD5);
else
bw.Write(RegionHashes[i], 0, 16);
}
// Number of files hashed
bw.Write(FileCount);
@@ -1161,21 +1338,25 @@ namespace LibIRD
// IRD Unique Identifier, for versions after 7
if (_version > 7)
bw.Write(UID);
}
// Calculate the little-endian 32-bit "IEEE 802.3" CRC value of the IRD contents so far
stream.Position = 0;
Crc32 crc32 = new();
crc32.Append(stream);
byte[] crc = crc32.GetCurrentHash();
byte[] crc32 = HashTool.GetStreamHashArray(stream, HashType.CRC32, true);
Array.Reverse(crc32);
// Write final CRC value to the stream
stream.Write(crc, 0, 4);
stream.Write(crc32, 0, 4);
// Create the IRD file stream
using FileStream fs = new(irdPath, FileMode.Create, FileAccess.Write);
// Create a GZipped IRD file stream
#if NET6_0_OR_GREATER
using GZipStream gzStream = new(fs, CompressionLevel.SmallestSize);
#elif NETCOREAPP || NET45_OR_GREATER
using GZipStream gzStream = new(fs, CompressionLevel.Optimal);
#else
using GZipStream gzStream = new(fs, CompressionMode.Compress);
#endif
// Write entire gzipped IRD stream to file
stream.Position = 0;
stream.CopyTo(gzStream);
@@ -1192,7 +1373,7 @@ namespace LibIRD
// Validate irdPath
if (irdPath == null || irdPath.Length <= 0)
throw new ArgumentNullException(nameof(irdPath));
if (!File.Exists(irdPath))
if (!System.IO.File.Exists(irdPath))
throw new FileNotFoundException(nameof(irdPath));
// Open IRD for reading
@@ -1202,7 +1383,7 @@ namespace LibIRD
// Check for IRD file signature
byte[] magic = br.ReadBytes(4);
if (!((ReadOnlySpan<byte>)Magic).SequenceEqual(magic))
if (magic == null || magic.Length != 4 || magic[0] != Magic[0] || magic[1] != Magic[1] || magic[2] != Magic[2] || magic[3] != Magic[3])
throw new InvalidDataException("IRD file not recognised");
// Read file version
@@ -1321,9 +1502,9 @@ namespace LibIRD
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($"Data 1 Key: {ByteArrayToHexString(Data1Key)}");
printText.AppendLine($"Data 2 Key: {ByteArrayToHexString(Data2Key)}");
printText.AppendLine($"PIC: {ByteArrayToHexString(PIC)}");
printText.AppendLine();
if (printPath == null)
@@ -1336,7 +1517,7 @@ namespace LibIRD
}
else
{
File.AppendAllText(printPath, printText.ToString());
System.IO.File.AppendAllText(printPath, printText.ToString());
}
}
@@ -1366,9 +1547,9 @@ namespace LibIRD
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($" \"Data 1 Key\": \"{ByteArrayToHexString(Data1Key)}\",");
json.AppendLine($" \"Data 2 Key\": \"{ByteArrayToHexString(Data2Key)}\",");
json.AppendLine($" \"PIC\": \"{ByteArrayToHexString(PIC)}\"");
if (single)
json.AppendLine("}");
else
@@ -1386,10 +1567,70 @@ namespace LibIRD
else
{
// Write to path
File.AppendAllText(jsonPath, json.ToString());
System.IO.File.AppendAllText(jsonPath, json.ToString());
}
}
#endregion
#region Helper Functions
/// <summary>
/// Converts a hex string into a byte array
/// </summary>
/// <param name="hex">Hex string</param>
/// <returns>Converted byte array, or null if invalid hex string</returns>
public static byte[] HexStringToByteArray(string hexString)
{
// Valid hex string must be an even number of characters
if (string.IsNullOrEmpty(hexString) || hexString!.Length % 2 == 1)
return null;
// Convert ASCII to byte via lookup table
int[] hexLookup = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F];
byte[] byteArray = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
// Convert next two chars to ASCII value relative to '0'
int a = Char.ToUpper(hexString[i]) - '0';
int b = Char.ToUpper(hexString[i + 1]) - '0';
// Ensure hex string only has '0' through '9' and 'A' through 'F' (case insensitive)
if ((a < 0 || b < 0 || a > 22 || b > 22) || (a > 10 && a < 17) || (b > 10 && b < 17))
return null;
byteArray[i / 2] = (byte)(hexLookup[a] << 4 | hexLookup[b]);
}
return byteArray;
}
/// <summary>
/// Converts a byte array into a hex string
/// </summary>
/// <param name="byteArray">Byte array</param>
/// <returns>Hex string representation of byte array, null if invalid</returns>
public static string ByteArrayToHexString(byte[] byteArray)
{
// Validate byte array
if (byteArray == null || byteArray.Length == 0)
return null;
// Store map of values to hex string
string hex = "0123456789ABCDEF";
// Add two characters per byte value
StringBuilder hexString = new(2 * byteArray.Length);
foreach (byte b in byteArray)
{
hexString.Append(hex[(int)(b >> 4)]);
hexString.Append(hex[(int)(b & 0x0F)]);
}
// Return hex string
return hexString.ToString();
}
#endregion
}
}
+8 -7
View File
@@ -2,16 +2,19 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<LangVersion>latest</LangVersion>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>0.3.0</Version>
<Version>0.7.0</Version>
<PackageOutputPath>../nupkg</PackageOutputPath>
<!-- Package Properties -->
<Authors>Deterous</Authors>
<Description>Library for ISO Rebuild Data</Description>
<Copyright>Copyright (c) Deterous 2024</Copyright>
<Copyright>Copyright (c) Deterous 2023-2024</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/Deterous/LibIRD/</RepositoryUrl>
<RepositoryType>git</RepositoryType>
@@ -25,10 +28,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Core\DiscUtils.Core.csproj" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Iso9660\DiscUtils.Iso9660.csproj" />
<ProjectReference Include="..\DiscUtils\Library\DiscUtils.Streams\DiscUtils.Streams.csproj" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SabreTools.Hashing" Version="1.1.3" />
</ItemGroup>
</Project>
+3 -7
View File
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using Newtonsoft.Json;
namespace LibIRD
{
@@ -33,12 +33,8 @@ namespace LibIRD
/// 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);
@@ -72,7 +68,7 @@ namespace LibIRD
// Read SFB file version
byte[] buf = br.ReadBytes(2);
Array.Reverse(buf);
Version = BitConverter.ToUInt16(buf);
Version = BitConverter.ToUInt16(buf, 0);
// Process all field headers
sfbStream.Seek(0x20, SeekOrigin.Begin);
@@ -142,7 +138,7 @@ namespace LibIRD
public void PrintJson(string jsonPath = null)
{
// Serialise PS3_Disc.SFB data to a JSON object
string json = JsonSerializer.Serialize(Field, new JsonSerializerOptions { WriteIndented = true });
string json = JsonConvert.SerializeObject(Field, Formatting.Indented);
// If no path given, output to console
if (jsonPath == null)
+6 -11
View File
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using Newtonsoft.Json;
namespace LibIRD
{
@@ -43,13 +43,8 @@ namespace LibIRD
/// Constructor using a PARAM.SFO file path
/// </summary>
/// <param name="sfoPath">Full file path to the PARAM.SFO file</param>
/// <exception cref="ArgumentNullException"></exception>
public ParamSFO(string sfoPath)
{
// Validate file path
if (sfoPath == null || sfoPath.Length <= 0)
throw new ArgumentNullException(nameof(sfoPath));
// Read file as a stream, and parse file
using FileStream fs = new(sfoPath, FileMode.Open, FileAccess.Read);
Parse(fs);
@@ -103,7 +98,7 @@ namespace LibIRD
- keyOffset[i];
// Read ith key name
string key = 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 + dataOffset[i];
@@ -112,13 +107,13 @@ namespace LibIRD
Field[key] = dataFormat[i] switch
{
// Non-null-terminated UTF-8 String
0x0004 => Encoding.UTF8.GetString(br.ReadBytes((int) dataLength[i])),
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'),
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'),
_ => Encoding.UTF8.GetString(br.ReadBytes((int)dataLength[i])).TrimEnd('\0'),
};
}
}
@@ -166,7 +161,7 @@ namespace LibIRD
public void PrintJson(string jsonPath = null)
{
// Serialise PS3_Disc.SFB data to a JSON object
string json = JsonSerializer.Serialize(Field, new JsonSerializerOptions { WriteIndented = true });
string json = JsonConvert.SerializeObject(Field, Formatting.Indented);
// If no path given, output to console
if (jsonPath == null)
+34 -77
View File
@@ -1,6 +1,5 @@
using System;
using System.IO;
using System.IO.Hashing;
namespace LibIRD
{
@@ -51,15 +50,6 @@ namespace LibIRD
/// </summary>
public class ReIRD : IRD
{
#region Properties
/// <summary>
/// ISO file size
/// </summary>
private long Size { get; set; }
#endregion
#region Constructors
/// <summary>
@@ -67,26 +57,7 @@ namespace LibIRD
/// </summary>
/// <param name="isoPath">Path to the ISO</param>
/// <param name="getKeyLog">Path to the GetKey log file</param>
/// <param name="layerbreak">Layerbreak value, in sectors</param>
/// <exception cref="InvalidDataException"></exception>
public ReIRD(string isoPath, string getKeyLog, long? layerbreak = null) : base(isoPath, getKeyLog, true)
{
// Generate Unique Identifier using ISO CRC32
UID = GenerateUID(isoPath);
// Determine ISO file size
Size = CalculateSize(isoPath);
// Generate Data 2 using Disc ID
DiscID = GenerateID(Size);
// Generate Disc PIC
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");
}
public ReIRD(string isoPath, string getKeyLog) : base(isoPath, getKeyLog, true) { }
/// <summary>
/// Constructor with optional additional region to generate a specific Disc ID
@@ -94,23 +65,24 @@ namespace LibIRD
/// <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="uid">Unique ID, crc32 hash of ISO</param>
/// <param name="region">Disc Region</param>
public ReIRD(string isoPath, byte[] key, long? layerbreak = null, Region region = Region.NONE) : base()
public ReIRD(string isoPath, byte[] key, long? layerbreak = null, uint? uid = null, Region region = Region.NONE) : base()
{
// Generate Unique Identifier using ISO CRC32
UID = GenerateUID(isoPath);
// If the ISO CRC32 is provided, use it as the Unique ID
UID = uid == null ? 0x00000000 : (uint)uid;
// Determine ISO file size
Size = CalculateSize(isoPath);
long size = CalculateSize(isoPath);
// Set Disc Key
DiscKey = key;
// Generate Data 2 using Disc ID
DiscID = GenerateID(Size, region);
DiscID = GenerateID(size, region);
// Generate Disc PIC
PIC = GeneratePIC(Size, layerbreak * SectorSize);
PIC = GeneratePIC(isoPath, size, layerbreak * SectorSize);
// Generate IRD fields
GenerateIRD(isoPath, true);
@@ -143,12 +115,13 @@ 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 = null, bool exactIRD = false)
private static byte[] GeneratePIC(string isoPath, long size, long? layerbreak = null, bool exactIRD = false)
{
// Validate size
if (size <= 0 || (size % SectorSize) != 0)
throw new ArgumentException("ISO Size in bytes must be a positive integer multiple of 2048", nameof(size));
// Validate layerbreak
// Validate provided layerbreak
if (layerbreak != null)
{
if (layerbreak <= 0 || (layerbreak >= size))
@@ -156,8 +129,16 @@ namespace LibIRD
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;
else if (size > BDLayerSize)
{
// If no layerbreak provided, ensure ISO is not BD-Video hybrid
using FileStream fs = new FileStream(isoPath, FileMode.Open, FileAccess.Read) ?? throw new FileNotFoundException(isoPath);
DiscUtils.Iso9660.CDReader reader = new(fs);
if (reader.DirectoryExists("\\BDMV"))
throw new ArgumentException("Layerbreak must be provided for BD-Video hybrid discs");
// Assume disc has default layerbreak
layerbreak = BDLayerSize;
}
// Generate the PIC based on the size and layerbreak of the ISO
byte[] pic;
@@ -167,7 +148,7 @@ namespace LibIRD
long l0_start_sector = 1048576;
// Layer 0 end sector = start sector + layerbreak - 2
long l0_end_sector = (layer_break / SectorSize) + l0_start_sector - 2;
long l0_end_sector = ((long)layerbreak / SectorSize) + l0_start_sector - 2;
// Convert end sector location to hex values for PIC
byte[] l0es = [(byte)((l0_end_sector >> 24) & 0xFF),
(byte)((l0_end_sector >> 16) & 0xFF),
@@ -175,7 +156,7 @@ namespace LibIRD
(byte)((l0_end_sector >> 0) & 0xFF)];
// Layer 1 start sector = end of disc (0x01EFFFFE) - layerbreak + 2
long l1_start_sector = 32505854 - (layer_break / SectorSize) + 2;
long l1_start_sector = 32505854 - ((long)layerbreak! / SectorSize) + 2;
// Convert start of start sector location to hex values for PIC
byte[] l1ss = [(byte)((l1_start_sector >> 24) & 0xFF),
(byte)((l1_start_sector >> 16) & 0xFF),
@@ -184,7 +165,7 @@ namespace LibIRD
// 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);
byte[] ts = BitConverter.GetBytes((uint)total_sectors);
// Define the PIC
pic = [
@@ -249,48 +230,24 @@ namespace LibIRD
return pic;
}
/// <summary>
/// Generates the UID field by computing the CRC32 hash of the ISO
/// </summary>
/// <param name="isoPath">Path to the ISO</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="FileNotFoundException"></exception>
private static uint GenerateUID(string isoPath)
{
// Validate ISO path
ArgumentNullException.ThrowIfNull(isoPath, nameof(isoPath));
// Check file exists
var iso = new FileInfo(isoPath);
if (!iso.Exists)
throw new FileNotFoundException(nameof(isoPath));
// Compute CRC32 hash
byte[] crc32;
using (FileStream fs = File.OpenRead(isoPath))
{
Crc32 hasher = new();
hasher.Append(fs);
crc32 = hasher.GetCurrentHash();
}
// Redump ISO CRC32 hash is used as the Unique ID in the reproducible IRD
return BitConverter.ToUInt32(crc32, 0);
}
/// <summary>
/// Calculates ISO file size
/// </summary>
/// <param name="isoPath">Path to the ISO</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="FileNotFoundException"></exception>
private static long CalculateSize(string isoPath)
{
// Validate ISO path
ArgumentNullException.ThrowIfNull(isoPath, nameof(isoPath));
// Check file exists
var iso = new FileInfo(isoPath);
FileInfo iso;
try
{
iso = new FileInfo(isoPath);
}
catch (Exception e)
{
throw new ArgumentException("Invalid ISO Path: " + e.Message);
}
if (!iso.Exists)
throw new FileNotFoundException(nameof(isoPath));
+2
View File
@@ -13,6 +13,8 @@ IRDKit is a tool that allows direct use of LibIRD functionality from a command l
- 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`
- Renaming a folder of IRDs using a redump DAT: `irdkit rename -d redump.dat IRDs\`
For detailed usage, read more [here](IRDKit).
## Using the LibIRD library
+15
View File
@@ -0,0 +1,15 @@
dotnet publish -c Release -f net8.0 -r win-x86 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r win-x64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r win-arm64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r linux-x64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r linux-arm64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r osx-x64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
dotnet publish -c Release -f net8.0 -r osx-arm64 --self-contained=false -p:PublishSingleFile=true -p:DebugType=None IRDKit\IRDKit.csproj
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/win-x86/publish/irdkit.exe" -Destination "./irdkit-win-x86.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/win-x64/publish/irdkit.exe" -Destination "./irdkit-win-x64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/win-arm64/publish/irdkit.exe" -Destination "./irdkit-win-arm64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/linux-x64/publish/irdkit" -Destination "./irdkit-linux-x64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/linux-arm64/publish/irdkit" -Destination "./irdkit-linux-arm64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/osx-x64/publish/irdkit" -Destination "./irdkit-osx-x64.zip" -CompressionLevel "Optimal"
Compress-Archive -Path "./IRDKit/bin/Release/net8.0/osx-arm64/publish/irdkit" -Destination "./irdkit-osx-arm64.zip" -CompressionLevel "Optimal"