Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ records, not how the code changed.

## [Unreleased]

### Added

- **LZFSE-compressed files are read on HFS+ and APFS.** Files macOS
compresses with LZFSE (the method newer macOS versions prefer) were listed
with a note and a volume finding but their content was not shown. They are
now decompressed like zlib and LZVN ones. The decoder was checked against
streams written by Apple's own reference encoder; only files stored raw
(compression types 9 and 10) are still listed without content. A cached
HFS+ or APFS filesystem tree is rebuilt the next time it is opened, so
these files show their size. Tested on synthetic volumes only.

## [0.8.0] - 2026-10-01

Adds RAID 0, 1 and 5 assembly, QCOW2 and VirtualBox VDI disk images,
Expand Down
7 changes: 4 additions & 3 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ recognised and refused by name today; these entries are about *reading* it.
fork) are decompressed on HFS+ and APFS; HFS+ file hard links read as
the file they stand for, and directory hard links are listed with
their target. Tested on synthetic volumes (an independent reader,
libfshfs, confirmed the builder's hard-link layout). Not done: LZFSE
compressed files (listed, contents not shown), APFS hard links, and
**HFS** (the original, not HFS+).
libfshfs, confirmed the builder's hard-link layout). LZFSE is read too,
checked against Apple's reference encoder. Not done: files stored raw
(types 9 and 10), APFS hard links, and **HFS** (the original, not
HFS+).
- [~] **APFS snapshots** — snapshots are listed and can be opened as a
read-only view of the volume. Tested on synthetic images only so far.

Expand Down
23 changes: 17 additions & 6 deletions engine/decmpfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
7 LZVN attribute
8 LZVN resource fork
9/10 stored raw (attribute / resource fork; not read here)
11/12 LZFSE (attribute / resource fork; not read here)
11/12 LZFSE (attribute / resource fork)

The numbering and the block tables follow libfshfs and Apple's published
LZFSE sources; only zlib and LZVN are decoded. A block that is stored
uncompressed starts with a marker byte (0xFF for zlib, 0x06 for LZVN) and
carries the data after it."""
LZFSE sources; zlib, LZVN and LZFSE are decoded. A block that is stored
uncompressed starts with a marker byte (0xFF for zlib and LZFSE, 0x06 for
LZVN) and carries the data after it."""

import struct
import zlib
Expand All @@ -45,7 +45,7 @@
9: (CODEC_RAW, False), 10: (CODEC_RAW, True),
11: (CODEC_LZFSE, False), 12: (CODEC_LZFSE, True),
}
SUPPORTED = (CODEC_ZLIB, CODEC_LZVN)
SUPPORTED = (CODEC_ZLIB, CODEC_LZVN, CODEC_LZFSE)


class DecmpfsError(ValueError):
Expand Down Expand Up @@ -212,7 +212,18 @@ def _lzvn_block(data, expected):
return lzvn_decode(data, expected)


_BLOCK_DECODERS = {CODEC_ZLIB: _zlib_block, CODEC_LZVN: _lzvn_block}
def _lzfse_block(data, expected):
if data[:1] == b"\xff": # stored: 0xFF, then the data
return bytes(data[1:1 + expected])
from . import lzfse # (it uses this module's LZVN)
try:
return lzfse.decode(data, max_size=max(expected, 1))[:expected]
except lzfse.LzfseError as exc:
raise DecmpfsError(str(exc))


_BLOCK_DECODERS = {CODEC_ZLIB: _zlib_block, CODEC_LZVN: _lzvn_block,
CODEC_LZFSE: _lzfse_block}


class Compressed:
Expand Down
Loading
Loading