Skip to content

Commit f5f0f0f

Browse files
committed
Add APFS implementation
1 parent 9988c5b commit f5f0f0f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+8136
-0
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tests/_data/** filter=lfs diff=lfs merge=lfs -text

.github/pull_request_template.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!--
2+
Thank you for submitting a Pull Request. Please:
3+
* Read our commit style guide:
4+
Commit messages should adhere to the following points:
5+
* Separate subject from body with a blank line
6+
* Limit the subject line to 50 characters as much as possible
7+
* Capitalize the subject line
8+
* Do not end the subject line with a period
9+
* Use the imperative mood in the subject line
10+
* The verb should represent what was accomplished (Create, Add, Fix etc)
11+
* Wrap the body at 72 characters
12+
* Use the body to explain the what and why vs. the how
13+
For an example, look at the following link:
14+
https://docs.dissect.tools/en/latest/contributing/style-guide.html#example-commit-message
15+
16+
* Include a description of the proposed changes and how to test them.
17+
18+
* After creation, associate the PR with an issue, under the development section.
19+
Or use closing keywords in the body during creation:
20+
E.G:
21+
* close(|s|d) #<nr>
22+
* fix(|es|ed) #<nr>
23+
* resolve(|s|d) #<nr>
24+
-->

.github/workflows/dissect-ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Dissect CI
2+
on:
3+
push:
4+
branches:
5+
- main
6+
tags:
7+
- '*'
8+
pull_request:
9+
workflow_dispatch:
10+
11+
jobs:
12+
ci:
13+
uses: fox-it/dissect-workflow-templates/.github/workflows/dissect-ci-template.yml@main
14+
secrets: inherit
15+
16+
publish:
17+
if: ${{ github.ref_name == 'main' || github.ref_type == 'tag' }}
18+
needs: [ci]
19+
runs-on: ubuntu-latest
20+
environment: dissect_publish
21+
permissions:
22+
id-token: write
23+
steps:
24+
- uses: actions/download-artifact@v4
25+
with:
26+
name: packages
27+
path: dist/
28+
# According to the documentation, it automatically looks inside the `dist/` folder for packages.
29+
- name: Publish package distributions to Pypi
30+
uses: pypa/gh-action-pypi-publish@release/v1
31+
32+
trigger-tests:
33+
needs: [publish]
34+
uses: fox-it/dissect-workflow-templates/.github/workflows/dissect-ci-demand-test-template.yml@main
35+
secrets: inherit
36+
with:
37+
on-demand-test: 'dissect.target'

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
coverage.xml
2+
.coverage
3+
dist/
4+
.eggs/
5+
*.egg-info/
6+
*.pyc
7+
__pycache__/
8+
.pytest_cache/
9+
tests/_docs/api
10+
tests/_docs/build
11+
.tox/

COPYRIGHT

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Dissect is released as open source by Fox-IT (https://www.fox-it.com) part of NCC Group Plc (https://www.nccgroup.com)
2+
3+
Developed by the Dissect Team (dissect@fox-it.com) and made available at https://github.com/fox-it/dissect.apfs
4+
5+
License terms: AGPL3 (https://www.gnu.org/licenses/agpl-3.0.html)

LICENSE

Lines changed: 661 additions & 0 deletions
Large diffs are not rendered by default.

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
exclude .git*
2+
recursive-exclude .github/ *

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# dissect.apfs
2+
3+
A Dissect module implementing a parser for the APFS file system, a commonly used Apple filesystem. For more
4+
information, please see [the documentation](https://docs.dissect.tools/en/latest/projects/dissect.apfs/index.html).
5+
6+
## Requirements
7+
8+
This project is part of the Dissect framework and requires Python.
9+
10+
Information on the supported Python versions can be found in the Getting Started section of [the documentation](https://docs.dissect.tools/en/latest/index.html#getting-started).
11+
12+
## Installation
13+
14+
`dissect.apfs` is available on [PyPI](https://pypi.org/project/dissect.apfs/).
15+
16+
```bash
17+
pip install dissect.apfs
18+
```
19+
20+
This module is also automatically installed if you install the `dissect` package.
21+
22+
## Build and test instructions
23+
24+
This project uses `tox` to build source and wheel distributions. Run the following command from the root folder to build
25+
these:
26+
27+
```bash
28+
tox -e build
29+
```
30+
31+
The build artifacts can be found in the `dist/` directory.
32+
33+
`tox` is also used to run linting and unit tests in a self-contained environment. To run both linting and unit tests
34+
using the default installed Python version, run:
35+
36+
```bash
37+
tox
38+
```
39+
40+
For a more elaborate explanation on how to build and test the project, please see [the
41+
documentation](https://docs.dissect.tools/en/latest/contributing/tooling.html).
42+
43+
## Contributing
44+
45+
The Dissect project encourages any contribution to the codebase. To make your contribution fit into the project, please
46+
refer to [the development guide](https://docs.dissect.tools/en/latest/contributing/developing.html).
47+
48+
## Copyright and license
49+
50+
Dissect is released as open source by Fox-IT (<https://www.fox-it.com>) part of NCC Group Plc
51+
(<https://www.nccgroup.com>).
52+
53+
Developed by the Dissect Team (<dissect@fox-it.com>) and made available at <https://github.com/fox-it/dissect>.
54+
55+
License terms: AGPL3 (<https://www.gnu.org/licenses/agpl-3.0.html>). For more information, see the LICENSE file.

dissect/apfs/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from dissect.apfs.apfs import APFS
2+
from dissect.apfs.exception import (
3+
Error,
4+
FileNotFoundError,
5+
NotADirectoryError,
6+
NotASymlinkError,
7+
)
8+
9+
__all__ = [
10+
"APFS",
11+
"Error",
12+
"FileNotFoundError",
13+
"NotADirectoryError",
14+
"NotASymlinkError",
15+
]

dissect/apfs/apfs.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, BinaryIO
4+
5+
from dissect.apfs.c_apfs import c_apfs
6+
from dissect.apfs.objects import NxSuperblock
7+
8+
if TYPE_CHECKING:
9+
from uuid import UUID
10+
11+
from dissect.apfs.objects.fs import FS
12+
from dissect.apfs.objects.keybag import ContainerKeybag
13+
14+
15+
class APFS:
16+
"""Container class for APFS operations.
17+
18+
Args:
19+
fh: File-like object to read the APFS container from.
20+
"""
21+
22+
def __init__(self, fh: BinaryIO):
23+
self.fh = fh
24+
self.fh.seek(0)
25+
26+
self.sb = NxSuperblock.from_block(self, 0, self.fh.read(c_apfs.NX_DEFAULT_BLOCK_SIZE))
27+
self.sb = sorted(
28+
[self.sb] + [obj for obj in self.sb.checkpoint_objects if isinstance(obj, NxSuperblock)],
29+
key=lambda obj: obj.xid,
30+
)[-1]
31+
32+
@property
33+
def block_size(self) -> int:
34+
"""The block size of the container."""
35+
return self.sb.block_size
36+
37+
@property
38+
def sectors_per_block(self) -> int:
39+
"""The number of 512-byte sectors per block."""
40+
return self.block_size // 512
41+
42+
@property
43+
def block_count(self) -> int:
44+
"""The total number of blocks in the container."""
45+
return self.sb.block_count
46+
47+
@property
48+
def uuid(self) -> UUID:
49+
"""The UUID of the container."""
50+
return self.sb.uuid
51+
52+
@property
53+
def keybag(self) -> ContainerKeybag | None:
54+
"""The container keybag, if present."""
55+
return self.sb.keylocker
56+
57+
@property
58+
def volumes(self) -> list[FS]:
59+
"""All the filesystems in the container."""
60+
return self.sb.filesystems
61+
62+
def _read_block(self, address: int, count: int = 1) -> bytes:
63+
"""Read a block from the container.
64+
65+
Args:
66+
address: The block address to read.
67+
"""
68+
# TODO: Fusion tier2
69+
self.fh.seek(address * self.block_size)
70+
return self.fh.read(count * self.block_size)

0 commit comments

Comments
 (0)