Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions mkpfs/pfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ class PFSCHeader:
logical_block_size: Logical PFSC block size.
block_offsets_offset: Offset to the block offset table from payload start.
data_offset: Absolute offset where PFSC block data begins.
data_length: Logical padded byte length managed by the PFSC stream.
data_length: Logical original byte length managed by the PFSC stream.
"""

magic: int
Expand Down Expand Up @@ -1087,14 +1087,15 @@ def encode_pfsc_payload(
offsets: list[int] = [header_size]
for block in encoded_blocks:
offsets.append(offsets[-1] + len(block))
raw_length: int = len(raw)
header: PFSCHeader = PFSCHeader(
magic=consts.PFSC_MAGIC,
unk4=consts.PFSC_UNK4,
unk8=consts.PFSC_UNK8,
logical_block_size=logical_block_size,
block_offsets_offset=consts.PFSC_BLOCK_OFFSETS_OFFSET,
data_offset=header_size,
data_length=block_count * logical_block_size,
data_length=raw_length,
)
header_area: bytearray = bytearray(header_size)
struct.pack_into(
Expand Down Expand Up @@ -1486,7 +1487,7 @@ def _encode_pfsc_into_handle(
logical_block_size=logical_block_size,
block_offsets_offset=consts.PFSC_BLOCK_OFFSETS_OFFSET,
data_offset=header_size,
data_length=block_count * logical_block_size,
data_length=raw_size,
)
header_area: bytearray = bytearray(header_size)
struct.pack_into(
Expand Down Expand Up @@ -2076,8 +2077,6 @@ def _parse_pfsc_header(head: bytes) -> tuple[int, int, int, int, int]:
raise ValueError("PFSC block size mismatch between block_sz and block_sz2")
if logical_size < 0:
raise ValueError("PFSC logical size is negative")
if logical_size % logical_block_size != 0:
raise ValueError("PFSC logical size is not aligned to the logical block size")
if block_offsets_offset < consts.PFSC_HEADER_SIZE:
raise ValueError("PFSC block offset table overlaps header")
if block_offsets_offset != consts.PFSC_BLOCK_OFFSETS_OFFSET:
Expand All @@ -2088,7 +2087,7 @@ def _parse_pfsc_header(head: bytes) -> tuple[int, int, int, int, int]:
if data_offset < consts.PFSC_INITIAL_DATA_OFFSET:
raise ValueError("PFSC data offset is smaller than the minimum compatible header span")

block_count: int = logical_size // logical_block_size
block_count: int = ceil_div(logical_size, logical_block_size) if logical_size > 0 else 0
return logical_block_size, block_count, block_offsets_offset, data_offset, logical_size


Expand Down Expand Up @@ -2157,16 +2156,19 @@ def decode_pfsc_payload(payload: bytes, expected_logical_size: int | None = None
stored_block: bytes = payload[offsets[idx] : offsets[idx + 1]]
logical_out.extend(_decode_pfsc_block(stored_block, logical_block_size, idx))

padded_logical_size: int = block_count * logical_block_size
logical_payload: bytes = bytes(logical_out)
if len(logical_payload) != logical_size:
raise ValueError(f"PFSC logical output size {len(logical_payload)} does not match header size {logical_size}")
if len(logical_payload) != padded_logical_size:
raise ValueError(
f"PFSC logical output size {len(logical_payload)} does not match padded size {padded_logical_size}"
)
if expected_logical_size is not None:
if expected_logical_size < 0:
raise ValueError("expected inode logical size is negative")
if expected_logical_size > logical_size:
raise ValueError(f"PFSC logical size {logical_size} is smaller than inode size {expected_logical_size}")
return logical_payload[:expected_logical_size]
return logical_payload
return logical_payload[:logical_size]


def decode_inode_payload(
Expand Down
84 changes: 83 additions & 1 deletion tests/mkpfs/test_pfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,57 @@ def test_compressed_images_store_direct_pfsc_payloads(self) -> None:
assert len(compressed_payload) == compressed_inode.size
assert compressed_payload[:4] == b"PFSC"

def test_compressed_image_pfsc_header_tracks_non_aligned_file_size(self) -> None:
"""Compressed image PFSC headers should store exact file sizes, not padded block spans."""
tmp_path: Path = self.make_temp_path()
src: Path = make_app_with_nested_dirs(tmp_path / "src")
non_aligned_file: Path = src / "data" / "non_aligned.bin"
raw: bytes = (b"A" * c.PFSC_LOGICAL_BLOCK_SIZE) + (b"B" * 1024)
non_aligned_file.write_bytes(raw)
out: Path = tmp_path / "non-aligned-pfsc.ffpfs"
build_pfs(
source_root=src,
output_path=out,
block_size=65536,
pfs_version=c.PFS_VERSION_PS4,
inode_bits=32,
case_insensitive=True,
signed=False,
compress=True,
threshold_gain=1,
cpu_count=1,
zlib_level=9,
dry_run=False,
verbose=False,
encrypted=False,
)

with out.open("rb") as fh:
header: pfs_mod.ParsedHeader = parse_image_header(fh)
inspection: pfs_mod.PFSImageInspection = inspect_pfs_image(image=out, source=src)
assert inspection.errors == []
compressed_inode: pfs_mod.ParsedInode = inspection.inodes[inspection.file_inodes["data/non_aligned.bin"]]
assert compressed_inode.is_compressed
compressed_payload: bytes = pfs_mod.read_image_inode_payload(fh, header, compressed_inode)
assert compressed_payload[:4] == b"PFSC"

_logical_block_size: int
block_count: int
_block_offsets_offset: int
_data_offset: int
logical_size: int
(
_logical_block_size,
block_count,
_block_offsets_offset,
_data_offset,
logical_size,
) = pfs_mod._parse_pfsc_header(compressed_payload)
assert logical_size == len(raw)
assert block_count == 2
assert logical_size != block_count * c.PFSC_LOGICAL_BLOCK_SIZE
assert pfs_mod.decode_pfsc_payload(payload=compressed_payload) == raw

def test_build_pfs_uses_custom_temp_folder_for_pfsc_spool_files(self) -> None:
"""Compressed builds should route PFSC spool files through the configured temp folder."""
tmp_path: Path = self.make_temp_path()
Expand Down Expand Up @@ -1024,7 +1075,38 @@ def test_pfsc_encode_decode_round_trip(self) -> None:
assert block_size2 == c.PFSC_LOGICAL_BLOCK_SIZE
assert block_offsets == c.PFSC_BLOCK_OFFSETS_OFFSET
assert data_start >= c.PFSC_INITIAL_DATA_OFFSET
assert data_length == 3 * c.PFSC_LOGICAL_BLOCK_SIZE
assert data_length == len(raw)

def test_pfsc_header_uses_exact_non_aligned_logical_size(self) -> None:
"""PFSC headers should preserve the exact logical size for non-aligned files."""
raw: bytes = (b"A" * c.PFSC_LOGICAL_BLOCK_SIZE) + (b"B" * 1024)
encoded: bytes
_gain_pct: float
_hypothetical_size: int
encoded, _gain_pct, _hypothetical_size = pfs_mod.encode_pfsc_payload(
raw=raw,
threshold_gain=1,
zlib_level=9,
logical_block_size=c.PFSC_LOGICAL_BLOCK_SIZE,
)

assert encoded[:4] == b"PFSC"
_logical_block_size: int
block_count: int
_block_offsets_offset: int
_data_offset: int
logical_size: int
(
_logical_block_size,
block_count,
_block_offsets_offset,
_data_offset,
logical_size,
) = pfs_mod._parse_pfsc_header(encoded)
assert logical_size == len(raw)
assert block_count == 2
assert logical_size != block_count * c.PFSC_LOGICAL_BLOCK_SIZE
assert pfs_mod.decode_pfsc_payload(payload=encoded) == raw

def test_pfsc_encode_reports_incremental_progress_bytes(self) -> None:
"""PFSC encoding should report raw bytes as each logical block is processed."""
Expand Down
Loading