FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
CHDStudio/PBPSharp/PbpFile.cs at master · purelogiccode/CHDStudio · GitHub
purelogiccode
/
CHDStudio
Public
Uh oh!
There was an error while loading.
Please reload this page
.
Notifications
You must be signed in to change notification settings
Fork
1
Star
40
Code
Issues
0
Pull requests
0
Actions
Projects
Wiki
Security and quality
0
Insights
Additional navigation options
Code
Issues
Pull requests
Actions
Projects
Wiki
Security and quality
Insights
Expand file tree
Breadcrumbs
CHDStudio
/
PBPSharp
/
PbpFile.cs
Copy path
More file actions
More file actions
Latest commit
History
History
History
450 lines (391 loc) · 17.6 KB
Breadcrumbs
CHDStudio
/
PBPSharp
/
PbpFile.cs
Copy path
File metadata and controls
450 lines (391 loc) · 17.6 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
using
System
.
Buffers
.
Binary
;
using
System
.
Text
;
using
PBPSharp
.
Models
;
namespace
PBPSharp
;
/// <summary>
/// Provides functionality to open and read PBP (EBOOT.PBP) files.
/// Supports single-disc and multi-disc PlayStation PBP files.
/// </summary>
/// <remarks>
/// Missing files are reported through the returned <see cref="PbpError" />
/// (<see cref="PbpError.FileNotFound" />) rather than exceptions, following the CSOSharp
/// convention for the compressed-image family.
/// </remarks>
public
sealed
class
PbpFile
:
IDisposable
{
private
readonly
bool
_ownsStream
;
private
bool
_disposed
;
private
Stream
_stream
;
/// <summary>
/// Initializes a new instance of the <see cref="PbpFile" /> class. Instances are created by
/// the static <see cref="Open(string, out PbpFile?)" /> and
/// <see cref="Open(Stream, bool, out PbpFile?)" /> factory methods.
/// </summary>
/// <param name="stream">The seekable stream containing the PBP data.</param>
/// <param name="ownsStream">Whether disposing this instance disposes <paramref name="stream" />.</param>
/// <param name="header">The parsed PBP header.</param>
/// <param name="sfoData">The parsed PARAM.SFO metadata.</param>
/// <param name="discs">The disc entries discovered in the PSAR section.</param>
private
PbpFile
(
Stream
stream
,
bool
ownsStream
,
PbpHeader
header
,
SfoData
sfoData
,
IReadOnlyList
<
PbpDiscInfo
>
discs
)
{
_stream
=
stream
;
_ownsStream
=
ownsStream
;
Header
=
header
;
SfoData
=
sfoData
;
Discs
=
discs
;
}
/// <summary>
/// The parsed PBP header containing resource offsets.
/// </summary>
public
PbpHeader
Header
{
get
;
}
/// <summary>
/// The SFO (PARAM.SFO) metadata parsed from the PBP.
/// </summary>
public
SfoData
SfoData
{
get
;
}
/// <summary>
/// The list of disc entries found in the PBP.
/// </summary>
public
IReadOnlyList
<
PbpDiscInfo
>
Discs
{
get
;
}
/// <summary>
/// Whether this is a multi-disc PBP.
/// </summary>
public
bool
IsMultiDisc
=>
Discs
.
Count
>
1
;
/// <summary>
/// The game title from SFO metadata.
/// </summary>
public
string
?
Title
=>
SfoData
.
GetString
(
SfoData
.
Keys
.
Title
)
;
/// <summary>
/// The disc ID from SFO metadata (first disc).
/// </summary>
public
string
?
DiscId
=>
SfoData
.
GetString
(
SfoData
.
Keys
.
DiscId
)
;
/// <summary>
/// The game category (e.g., "ME" for PS1 EBOOT).
/// </summary>
public
string
?
Category
=>
SfoData
.
GetString
(
SfoData
.
Keys
.
Category
)
;
/// <summary>
/// Disposes of the PBP file and releases associated resources.
/// </summary>
public
void
Dispose
(
)
{
if
(
_disposed
)
return
;
_disposed
=
true
;
if
(
_ownsStream
)
_stream
.
Dispose
(
)
;
_stream
=
null
!
;
}
/// <summary>
/// Opens a PBP file from the specified file path.
/// </summary>
/// <param name="path">The full path to the PBP file.</param>
/// <param name="pbp">
/// When this method returns, contains the opened <see cref="PbpFile" /> instance if successful;
/// otherwise, null.
/// </param>
/// <returns>A <see cref="PbpError" /> indicating the result of the operation.</returns>
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
;
}
}
/// <summary>
/// Opens a PBP from an existing stream.
/// </summary>
/// <param name="stream">The stream containing PBP data. Must be seekable and readable.</param>
/// <param name="ownsStream">Whether this instance should dispose the stream when disposed.</param>
/// <param name="pbp">
/// When this method returns, contains the opened <see cref="PbpFile" /> instance if successful;
/// otherwise, null.
/// </param>
/// <returns>A <see cref="PbpError" /> indicating the result of the operation.</returns>
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
;
}
}
/// <summary>
/// Reads and validates the 40-byte PBP header at the start of the stream.
/// </summary>
/// <param name="stream">The seekable PBP stream, positioned at the start of the file.</param>
/// <param name="header">
/// When this method returns, contains the parsed header when the magic is valid; otherwise,
/// the default value.
/// </param>
/// <returns>
/// <see cref="PbpError.None" /> on success; <see cref="PbpError.InvalidHeader" /> when the
/// stream is too short or the PBP magic does not match.
/// </returns>
private
static
PbpError
ReadHeader
(
Stream
stream
,
out
PbpHeader
header
)
{
header
=
default
;
Span
<
byte
>
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
;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The seekable PBP stream.</param>
/// <param name="header">The parsed PBP header providing the SFO offset.</param>
/// <param name="sfoData">
/// When this method returns, contains the parsed SFO entries, or an empty instance when the
/// SFO is absent or corrupt.
/// </param>
/// <returns>Always <see cref="PbpError.None" />.</returns>
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
<
SfoEntry
>
(
)
;
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
<=
(
ulong
)
remaining
&&
entry
.
Length
<=
int
.
MaxValue
)
entry
.
Value
=
ReadNullTerminatedString
(
stream
,
(
int
)
entry
.
Length
)
;
break
;
case
0x0404
:
entry
.
Value
=
ReadUInt32
(
stream
,
new
byte
[
4
]
)
;
break
;
}
entries
.
Add
(
entry
)
;
}
sfoData
.
Entries
=
entries
;
sfoData
.
Size
=
(
uint
)
Math
.
Min
(
(
ulong
)
sfoData
.
DataTableOffset
+
dataTableSize
,
uint
.
MaxValue
)
;
}
catch
{
// Best effort: keep whatever entries parsed before the failure. A malformed SFO
// table must not abort extraction of an otherwise readable PSAR.
}
return
PbpError
.
None
;
}
/// <summary>
/// Locates the disc entries inside the PSAR section. A "PSISOIMG0000" PSAR is a single-disc
/// container; a "PSTITLEIMG000000" PSAR is a multi-disc container whose disc positions are
/// read from the table at PSAR+0x200. Each position is then parsed into a
/// <see cref="PbpDiscInfo" />.
/// </summary>
/// <param name="stream">The seekable PBP stream.</param>
/// <param name="header">The parsed PBP header providing the DATA.PSAR offset.</param>
/// <param name="discs">
/// When this method returns, contains the discs discovered in the PSAR, in disc order.
/// </param>
/// <returns>
/// <see cref="PbpError.None" /> on success; <see cref="PbpError.InvalidPsarHeader" /> when the
/// PSAR does not identify itself as a PlayStation disc image.
/// </returns>
private
static
PbpError
ReadDiscs
(
Stream
stream
,
PbpHeader
header
,
out
List
<
PbpDiscInfo
>
discs
)
{
discs
=
[
]
;
stream
.
Seek
(
header
.
DataPsarOffset
,
SeekOrigin
.
Begin
)
;
var
psarHeaderBuffer
=
new
byte
[
16
]
;
stream
.
ReadExactly
(
psarHeaderBuffer
,
0
,
16
)
;
var
psarHeader
=
Encoding
.
ASCII
.
GetString
(
psarHeaderBuffer
,
0
,
12
)
;
if
(
string
.
Equals
(
psarHeader
,
"PSISOIMG0000"
,
StringComparison
.
Ordinal
)
)
{
discs
.
Add
(
new
PbpDiscInfo
(
stream
,
header
.
DataPsarOffset
,
1
)
)
;
}
else
{
// Multi-disc: check for PSTITLEIMG000000
var
fullHeader
=
Encoding
.
ASCII
.
GetString
(
psarHeaderBuffer
,
0
,
16
)
;
if
(
!
string
.
Equals
(
fullHeader
,
"PSTITLEIMG000000"
,
StringComparison
.
Ordinal
)
)
return
PbpError
.
InvalidPsarHeader
;
// Skip past the header structure
var
skipBuffer
=
new
byte
[
4
]
;
stream
.
ReadExactly
(
skipBuffer
,
0
,
4
)
;
// padding
stream
.
ReadExactly
(
skipBuffer
,
0
,
4
)
;
// padding
// The next 16 bytes are a fixed "random" template in popstation/PSX2PSP/iPoPS
// (0x2CC9C5BC, 0x33B5A90F, 0x06F6B4B3, 0xB25945BA), but pop-fe leaves them zero in
// its multi-disc PBPs and they are still perfectly readable. The disc position
// table at +0x200 is what actually locates the discs, so tolerate any value here
// and let the per-disc parsing validate the real structure.
stream
.
ReadExactly
(
skipBuffer
,
0
,
4
)
;
stream
.
ReadExactly
(
skipBuffer
,
0
,
4
)
;
stream
.
ReadExactly
(
skipBuffer
,
0
,
4
)
;
stream
.
ReadExactly
(
skipBuffer
,
0
,
4
)
;
// The next 16 bytes are a fixed "random" template in popstation/PSX2PSP/iPoPS
// (0x2CC9C5BC, 0x33B5A90F, 0x06F6B4B3, 0xB25945BA), but pop-fe leaves them zero in
// its multi-disc PBPs and they are still perfectly readable. The disc position
// table at +0x200 is what actually locates the discs, so tolerate any value here
// and let the per-disc parsing validate the real structure.
// Skip 0x76 uint32 values
var
dummyBuffer
=
new
byte
[
4
]
;
for
(
var
i
=
0
;
i
<
0x76
;
i
++
)
stream
.
ReadExactly
(
dummyBuffer
,
0
,
4
)
;
// Read up to 5 disc positions
var
posBuffer
=
new
byte
[
20
]
;
// 5 * 4 bytes
stream
.
ReadExactly
(
posBuffer
,
0
,
20
)
;
for
(
var
i
=
0
;
i
<
5
;
i
++
)
{
var
pos
=
BitConverter
.
ToUInt32
(
posBuffer
,
i
*
4
)
;
if
(
pos
>
0
)
discs
.
Add
(
new
PbpDiscInfo
(
stream
,
header
.
DataPsarOffset
+
(
int
)
pos
,
i
+
1
)
)
;
}
}
return
PbpError
.
None
;
}
/// <summary>
/// Reads exactly four bytes from the stream and interprets them as a little-endian 32-bit
/// unsigned integer.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">A reusable four-byte buffer used to hold the raw bytes.</param>
/// <returns>The value read from the stream.</returns>
private
static
uint
ReadUInt32
(
Stream
stream
,
byte
[
]
buffer
)
{
stream
.
ReadExactly
(
buffer
,
0
,
4
)
;
return
BitConverter
.
ToUInt32
(
buffer
,
0
)
;
}
/// <summary>
/// Reads an ASCII string from the current stream position up to a null terminator or the
/// specified maximum length, whichever comes first.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="maxLength">The maximum number of bytes to read, including the terminator.</param>
/// <returns>The decoded string, without the null terminator.</returns>
private
static
string
ReadNullTerminatedString
(
Stream
stream
,
int
maxLength
)
{
var
buffer
=
new
byte
[
maxLength
]
;
var
length
=
0
;
for
(
var
i
=
0
;
i
<
maxLength
;
i
++
)
{
var
b
=
stream
.
ReadByte
(
)
;
if
(
b
<=
0
)
break
;
buffer
[
length
++
]
=
(
byte
)
b
;
}
return
Encoding
.
ASCII
.
GetString
(
buffer
,
0
,
length
)
;
}
}
Back
|
FazBrowse Home
|
New Git URL