Files
LibIRD/LibIRD/ParamSFO.cs
T
2024-02-05 09:20:59 +09:00

194 lines
6.8 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
namespace LibIRD
{
/// <summary>
/// PARAM.SFO file parsing
/// </summary>
public class ParamSFO
{
/// <summary>
/// PARAM.SFO file signature
/// </summary>
/// <remarks>{ 0x00, 0x50, 0x53, 0x46 }</remarks>
public static readonly string Magic = "\0PSF";
/// <summary>
/// PARAM.SFO file version
/// </summary>
/// <remarks>Typically { 0x01, 0x01, 0x00, 0x00 } (v1.1)</remarks>
public uint Version { get; private set; }
/// <summary>
/// A field within the PS3_DISC.SFB file
/// </summary>
/// <remarks>string Key, string Value</remarks>
public Dictionary<string, string> Field { get; private set; }
/// <summary>
/// Constructor using a PARAM.SFO file stream
/// </summary>
/// <param name="sfoStream">SFO file stream</param>
public ParamSFO(Stream sfoStream)
{
// Parse file stream
Parse(sfoStream);
}
/// <summary>
/// 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);
}
/// <summary>
/// Parse parameters and PARAM.SFO metadata from stream
/// </summary>
/// <param name="sfoStream">SFO file stream</param>
/// <exception cref="FileLoadException"></exception>
private void Parse(Stream sfoStream)
{
// Read binary stream
using BinaryReader br = new(sfoStream);
// Check file signature is correct
string magic = Encoding.ASCII.GetString(br.ReadBytes(4));
if (magic != ParamSFO.Magic)
throw new FileLoadException("Unexpected PARAM.SFO file");
// Parse header
Version = br.ReadUInt32();
uint keyTableStart = br.ReadUInt32();
uint dataTableStart = br.ReadUInt32();
uint paramCount = br.ReadUInt32();
// Parse parameter metadata
ushort[] keyOffset = new ushort[paramCount];
uint[] dataFormat = new uint[paramCount];
uint[] dataLength = new uint[paramCount];
uint[] dataTotal = new uint[paramCount];
uint[] dataOffset = new uint[paramCount];
for (int i = 0; i < paramCount; i++)
{
keyOffset[i] = br.ReadUInt16();
dataFormat[i] = br.ReadUInt16();
dataLength[i] = br.ReadUInt32();
dataTotal[i] = br.ReadUInt32();
dataOffset[i] = br.ReadUInt32();
}
// Parse parameters
Field = [];
for (int i = 0; i < paramCount; i++)
{
// Move stream to ith key
sfoStream.Position = keyTableStart + keyOffset[i];
// Determine ith key length
uint keyLen = ((i == paramCount - 1) ? dataTableStart - keyTableStart : keyOffset[i + 1])
- keyOffset[i];
// Read ith key name
string key = Encoding.ASCII.GetString(br.ReadBytes((int)keyLen)).TrimEnd('\0');
// Move stream to ith data
sfoStream.Position = dataTableStart + dataOffset[i];
// Read ith data, based on data format
Field[key] = dataFormat[i] switch
{
// Non-null-terminated UTF-8 String
0x0004 => Encoding.UTF8.GetString(br.ReadBytes((int)dataLength[i])),
// Null-terminated UTF-8 String
0x0204 => Encoding.UTF8.GetString(br.ReadBytes((int)dataLength[i])).TrimEnd('\0'),
// Integer
0x0404 => br.ReadInt32().ToString(),
// Unknown data format, assume null-terminated string
_ => Encoding.UTF8.GetString(br.ReadBytes((int)dataLength[i])).TrimEnd('\0'),
};
}
}
/// <summary>
/// Prints formatted parameters extracted from PARAM.SFO to console
/// </summary>
/// <param name="printPath">Optionally print to text file</param>
public void Print(string printPath = null, string isoName = null)
{
// Build string from parameters
StringBuilder printText = new();
if (isoName != null)
printText.AppendLine($"PARAM.SFO Contents: {isoName}");
else
printText.AppendLine("PARAM.SFO Contents:");
printText.AppendLine("===================");
// Loop through all parameters in PARAM.SFO
foreach (KeyValuePair<string, string> field in Field)
printText.AppendLine(field.Key + ": " + field.Value);
// Blank line
printText.Append(Environment.NewLine);
// If no path given, print to console
if (printPath == null)
{
// Ensure UTF-8 will display properly in console
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string to console
Console.Write(printText);
}
else
{
// Write data to file
File.AppendAllText(printPath, printText.ToString());
}
}
/// <summary>
/// Define JSON options once
/// </summary>
private readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true };
/// <summary>
/// Prints parameters extracted from PARAM.SFO to a json object
/// </summary>
/// <param name="jsonPath">Optionally print to json file</param>
public void PrintJson(string jsonPath = null)
{
// Serialise PS3_Disc.SFB data to a JSON object
string json = JsonSerializer.Serialize(Field, JsonOpts);
// If no path given, output to console
if (jsonPath == null)
{
// Ensure UTF-8 will display properly in console
Console.OutputEncoding = Encoding.UTF8;
// Print formatted string to console
Console.Write(json);
}
else
{
// Write to path
File.AppendAllText(jsonPath, json);
}
}
}
}