Skip to content
Open
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
22 changes: 22 additions & 0 deletions docs/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| [`AIROHA BT FIRMWARE`](#airoha-bt-firmware) | ARCHIVE | :octicons-alert-fill-12: |
| [`ANDROID EROFS`](#android-erofs) | FILESYSTEM | :octicons-check-16: |
| [`ANDROID SPARSE`](#android-sparse) | FILESYSTEM | :octicons-check-16: |
| [`APPLE ARCHIVE`](#apple-archive) | ARCHIVE | :octicons-alert-fill-12: |
| [`AR`](#ar) | ARCHIVE | :octicons-check-16: |
| [`ARC`](#arc) | ARCHIVE | :octicons-check-16: |
| [`ARJ`](#arj) | ARCHIVE | :octicons-check-16: |
Expand Down Expand Up @@ -170,6 +171,27 @@

- [Android Sparse Image Format Documentation](https://formats.kaitai.io/android_sparse/){ target="_blank" }
- [simg2img Tool](https://github.com/anestisb/android-simg2img){ target="_blank" }
## Apple Archive

!!! warning "Partially supported"

=== "Description"

Apple Archive is Apple's proprietary archive format introduced with macOS Big Sur, used for distributing macOS software updates and installers. Files begin with the AA01 magic and contain field-tagged entries encoding paths, LZFSE-compressed data blobs, symbolic links, and extended attributes.

---

- **Handler type:** Archive
- **Vendor:** Apple

=== "References"

- [Apple Archive - Apple Developer Documentation](https://developer.apple.com/documentation/applearchive){ target="_blank" }

=== "Limitations"

- Only PATP/DATA/LNKP/XATA/TYP1 field tags are handled; other tags are silently skipped
- Symlink targets are logged but not created in the output directory
## AR

!!! success "Fully supported"
Expand Down
2 changes: 2 additions & 0 deletions python/unblob/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .archive import (
zip as ziparchive,
)
from .archive.apple import applearchive
from .archive.autel import ecc
from .archive.dlink import alpha_encimg, deafbead, encrpted_img, fpkg, shrs
from .archive.engeniustech import engenius
Expand Down Expand Up @@ -120,6 +121,7 @@
ar.ARHandler,
arc.ARCHandler,
arj.ARJHandler,
applearchive.AppleArchiveHandler,
cab.CABHandler,
msi.MsiHandler,
tar.TarUstarHandler,
Expand Down
Empty file.
158 changes: 158 additions & 0 deletions python/unblob/handlers/archive/apple/applearchive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import io
from pathlib import Path

import lzfse
from structlog import get_logger

from unblob.file_utils import (
Endian,
File,
FileSystem,
convert_int8,
convert_int64,
)
from unblob.models import (
Extractor,
ExtractResult,
Handler,
HandlerDoc,
HandlerType,
HexString,
Reference,
ValidChunk,
)

logger = get_logger()


def _field_patp(
file: File, _fs: FileSystem, current_path: str | None
) -> tuple[str | None, bool]:
# PATP: Path Property (1-byte length + string)
len_bytes = file.read(1)
if not len_bytes:
return current_path, False
length = convert_int8(len_bytes, Endian.LITTLE)
return file.read(length).decode("utf-8", errors="ignore"), True


def _field_data(
file: File, fs: FileSystem, current_path: str | None
) -> tuple[str | None, bool]:
# DATA: Data Property (8-byte size + LZFSE/Raw blob)
size_bytes = file.read(8)
if not size_bytes:
return current_path, False
blob_size = convert_int64(size_bytes, Endian.LITTLE)
compressed_data = file.read(blob_size)
if current_path:
try:
fs.write_bytes(Path(current_path), lzfse.decompress(compressed_data))
except Exception:
fs.write_bytes(Path(current_path), compressed_data)
return current_path, True


def _field_lnkp(
file: File, _fs: FileSystem, current_path: str | None
) -> tuple[str | None, bool]:
# LNKP: Symbolic Link Property (1-byte length + string)
len_bytes = file.read(1)
if not len_bytes:
return current_path, True
length = convert_int8(len_bytes, Endian.LITTLE)
target = file.read(length).decode("utf-8", errors="ignore")
if current_path:
logger.debug("atlas symlink found", source=current_path, target=target)
return current_path, True


def _field_xata(
file: File, _fs: FileSystem, current_path: str | None
) -> tuple[str | None, bool]:
# XATA: Extended Attributes (skip or parse CRC)
len_bytes = file.read(1)
if not len_bytes:
return current_path, True
length = convert_int8(len_bytes, Endian.LITTLE)
current_pos = file.tell()
file.seek(0, io.SEEK_END)
file_size = file.tell()
file.seek(current_pos)
skip_bytes = length + 4
if current_pos + skip_bytes > file_size:
logger.warning("Invalid XATA field length, stopping parse")
return current_path, False
file.seek(skip_bytes, io.SEEK_CUR)
return current_path, True


def _field_typ1(
file: File, _fs: FileSystem, current_path: str | None
) -> tuple[str | None, bool]:
# TYP1: Entry Type (1 byte)
file.seek(1, io.SEEK_CUR)
return current_path, True


_FIELD_HANDLERS = {
"PATP": _field_patp,
"DATA": _field_data,
"LNKP": _field_lnkp,
"XATA": _field_xata,
"TYP1": _field_typ1,
}


class AppleArchiveExtractor(Extractor):
def extract(self, inpath: Path, outdir: Path) -> ExtractResult:
fs = FileSystem(outdir)

with File.from_path(inpath) as file:
magic = file.read(4)
if magic != b"AA01":
return ExtractResult(reports=[])

current_path: str | None = None

while True:
field_tag = file.read(4)
if len(field_tag) < 4:
break
tag = field_tag.decode("ascii", errors="ignore")
handler = _FIELD_HANDLERS.get(tag)
if handler is None:
continue
current_path, ok = handler(file, fs, current_path)
if not ok:
break

return ExtractResult(reports=fs.problems)


class AppleArchiveHandler(Handler):
NAME = "apple_archive"
PATTERNS = [HexString("41 41 30 31")] # "AA01"
EXTRACTOR = AppleArchiveExtractor()

DOC = HandlerDoc(
name="Apple Archive",
description="Apple Archive is Apple's proprietary archive format introduced with macOS Big Sur, used for distributing macOS software updates and installers. Files begin with the AA01 magic and contain field-tagged entries encoding paths, LZFSE-compressed data blobs, symbolic links, and extended attributes.",
handler_type=HandlerType.ARCHIVE,
vendor="Apple",
references=[
Reference(
title="Apple Archive - Apple Developer Documentation",
url="https://developer.apple.com/documentation/applearchive",
),
],
limitations=[
"Only PATP/DATA/LNKP/XATA/TYP1 field tags are handled; other tags are silently skipped",
"Symlink targets are logged but not created in the output directory",
],
)

def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
file.seek(0, io.SEEK_END)
end_offset = file.tell()
return ValidChunk(start_offset=start_offset, end_offset=end_offset)
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Loading