FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

GitHub Viewer

using System.Buffers.Binary; using System.Text; using PBPSharp.Models; namespace PBPSharp; /// /// Provides functionality to open and read PBP (EBOOT.PBP) files. /// Supports single-disc and multi-disc PlayStation PBP files. /// /// /// Missing files are reported through the returned /// () rather than exceptions, following the CSOSharp /// convention for the compressed-image family. /// public sealed class PbpFile : IDisposable { private readonly bool _ownsStream; private bool _disposed; private Stream _stream; /// /// Initializes a new instance of the class. Instances are created by /// the static and /// factory methods. /// /// The seekable stream containing the PBP data. /// Whether disposing this instance disposes . /// The parsed PBP header. /// The parsed PARAM.SFO metadata. /// The disc entries discovered in the PSAR section. private PbpFile( Stream stream, bool ownsStream, PbpHeader header, SfoData sfoData, IReadOnlyList discs ) { _stream = stream; _ownsStream = ownsStream; Header = header; SfoData = sfoData; Discs = discs; } /// /// The parsed PBP header containing resource offsets. /// public PbpHeader Header { get; } /// /// The SFO (PARAM.SFO) metadata parsed from the PBP. /// public SfoData SfoData { get; } /// /// The list of disc entries found in the PBP. /// public IReadOnlyList Discs { get; } /// /// Whether this is a multi-disc PBP. /// public bool IsMultiDisc => Discs.Count > 1; /// /// The game title from SFO metadata. /// public string? Title => SfoData.GetString(SfoData.Keys.Title); /// /// The disc ID from SFO metadata (first disc). /// public string? DiscId => SfoData.GetString(SfoData.Keys.DiscId); /// /// The game category (e.g., "ME" for PS1 EBOOT). /// public string? Category => SfoData.GetString(SfoData.Keys.Category); /// /// Disposes of the PBP file and releases associated resources. /// public void Dispose() { if (_disposed) return; _disposed = true; if (_ownsStream) _stream.Dispose(); _stream = null!; } /// /// Opens a PBP file from the specified file path. /// /// The full path to the PBP file. /// /// When this method returns, contains the opened instance if successful; /// otherwise, null. /// /// A indicating the result of the operation. public static PbpError Open(string path, out PbpFile? pbp) { pbp = null; if (!File.Exists(path)) return PbpError.FileNotFound; try { var stream = File.OpenRead(path); var error = Open(stream, true, out pbp); if (error != PbpError.None) stream.Dispose(); return error; } catch (IOException ex) { PbpDiagnostics.SetDetail($"Failed to open '{path}': {ex.Message}"); return PbpError.IoError; } } /// /// Opens a PBP from an existing stream. /// /// The stream containing PBP data. Must be seekable and readable. /// Whether this instance should dispose the stream when disposed. /// /// When this method returns, contains the opened instance if successful; /// otherwise, null. /// /// A indicating the result of the operation. public static PbpError Open(Stream stream, bool ownsStream, out PbpFile? pbp) { pbp = null; if (stream is not { CanRead: true } || !stream.CanSeek) return PbpError.IoError; try { stream.Seek(0, SeekOrigin.Begin); var headerError = ReadHeader(stream, out var header); if (headerError != PbpError.None) return headerError; var sfoError = ReadSfo(stream, header, out var sfoData); if (sfoError != PbpError.None) return sfoError; var discError = ReadDiscs(stream, header, out var discs); if (discError != PbpError.None) return discError; pbp = new PbpFile(stream, ownsStream, header, sfoData, discs); return PbpError.None; } catch (EndOfStreamException) { // The stream ended before the structure it declares: the download is truncated or // incomplete, which is a different condition from a failing device or permission. return PbpError.TruncatedPsar; } catch (IOException ex) { PbpDiagnostics.SetDetail($"Failed to read the PBP stream: {ex.Message}"); return PbpError.IoError; } catch (NoIsoIndexException) { // The PSAR header parsed correctly but no ISO index entries followed: the file ends // before its data area, which is the signature of a truncated or incomplete download. return PbpError.TruncatedPsar; } catch (InvalidDataException) { return PbpError.CorruptFile; } catch (Exception) { return PbpError.CorruptFile; } } /// /// Reads and validates the 40-byte PBP header at the start of the stream. /// /// The seekable PBP stream, positioned at the start of the file. /// /// When this method returns, contains the parsed header when the magic is valid; otherwise, /// the default value. /// /// /// on success; when the /// stream is too short or the PBP magic does not match. /// private static PbpError ReadHeader(Stream stream, out PbpHeader header) { header = default; Span headerBytes = stackalloc byte[PbpHeader.HeaderSize]; try { stream.ReadExactly(headerBytes); } catch (EndOfStreamException) { return PbpError.InvalidHeader; } var magic = BinaryPrimitives.ReadUInt32LittleEndian(headerBytes[..4]); if (magic != PbpHeader.MagicValue) return PbpError.InvalidHeader; var version = BinaryPrimitives.ReadUInt32LittleEndian(headerBytes[4..8]); var sfoOffset = BinaryPrimitives.ReadInt32LittleEndian(headerBytes[8..12]); var icon0Offset = BinaryPrimitives.ReadInt32LittleEndian(headerBytes[12..16]); var icon1Offset = BinaryPrimitives.ReadInt32LittleEndian(headerBytes[16..20]); var pic0Offset = BinaryPrimitives.ReadInt32LittleEndian(headerBytes[20..24]); var pic1Offset = BinaryPrimitives.ReadInt32LittleEndian(headerBytes[24..28]); var snd0Offset = BinaryPrimitives.ReadInt32LittleEndian(headerBytes[28..32]); var dataPspOffset = BinaryPrimitives.ReadInt32LittleEndian(headerBytes[32..36]); var dataPsarOffset = BinaryPrimitives.ReadInt32LittleEndian(headerBytes[36..40]); header = new PbpHeader( version, sfoOffset, icon0Offset, icon1Offset, pic0Offset, pic1Offset, snd0Offset, dataPspOffset, dataPsarOffset ); return PbpError.None; } /// /// Reads the PARAM.SFO metadata section pointed to by the PBP header. Parsing is best /// effort: a missing or malformed SFO leaves the returned data empty instead of failing the /// open, because disc extraction does not depend on the metadata. /// /// The seekable PBP stream. /// The parsed PBP header providing the SFO offset. /// /// When this method returns, contains the parsed SFO entries, or an empty instance when the /// SFO is absent or corrupt. /// /// Always . private static PbpError ReadSfo(Stream stream, PbpHeader header, out SfoData sfoData) { sfoData = new SfoData(); try { stream.Seek(header.SfoOffset, SeekOrigin.Begin); var sfoBuffer = new byte[4]; sfoData.Magic = ReadUInt32(stream, sfoBuffer); sfoData.Version = ReadUInt32(stream, sfoBuffer); // A real SFO starts with the bytes 00 50 53 46 ("\0PSF"), which as a little-endian // uint32 is 0x46535000. A missing or corrupt SFO does not stop the run: none of the // reference tools read the SFO when extracting disc images from the PSAR, so metadata // is simply absent (Title/DiscId are null) rather than the whole file rejected. if (sfoData.Magic != 0x46535000) return PbpError.None; sfoData.KeyTableOffset = ReadUInt32(stream, sfoBuffer); sfoData.DataTableOffset = ReadUInt32(stream, sfoBuffer); var entryCount = ReadUInt32(stream, sfoBuffer); var entries = new List(); var dataTableSize = 0UL; for (var i = 0; i < entryCount; i++) { var dirBuffer = new byte[16]; stream.Seek(header.SfoOffset + 20 + i * 16, SeekOrigin.Begin); stream.ReadExactly(dirBuffer, 0, 16); // Layout: KeyOffset(2) + Format(2) + Length(4) + MaxLength(4) + DataOffset(4) var keyOffset = BinaryPrimitives.ReadUInt16LittleEndian(dirBuffer.AsSpan(0, 2)); var entry = new SfoEntry { Format = BinaryPrimitives.ReadUInt16LittleEndian(dirBuffer.AsSpan(2, 2)), Length = BinaryPrimitives.ReadUInt32LittleEndian(dirBuffer.AsSpan(4, 4)), MaxLength = BinaryPrimitives.ReadUInt32LittleEndian(dirBuffer.AsSpan(8, 4)) }; var dataOffset = BinaryPrimitives.ReadUInt32LittleEndian(dirBuffer.AsSpan(12, 4)); var entryEnd = (ulong)dataOffset + entry.MaxLength; if (entryEnd > dataTableSize) dataTableSize = entryEnd; stream.Seek(header.SfoOffset + sfoData.KeyTableOffset + keyOffset, SeekOrigin.Begin); entry.Key = ReadNullTerminatedString(stream, 128); stream.Seek(header.SfoOffset + sfoData.DataTableOffset + dataOffset, SeekOrigin.Begin); switch (entry.Format) { case 0x0204: // The declared length is untrusted; it cannot exceed what is left in the // file, and must fit an int so a corrupt entry cannot drive a multi-gigabyte // allocation or overflow the cast below. var remaining = stream.Length - stream.Position; if (remaining > 0 && entry.Length

Back | FazBrowse Home | New Git URL