saadtahir-dev/FIMountKit
GitHub: saadtahir-dev/FIMountKit
Stars: 0 | Forks: 0
# FIMountKit
Swift Package for mounting forensic and general-purpose disk images on macOS.
| | |
|---|---|
| **SPM package name** | `FIMountKit` |
| **Library product** | `FIMountKit` — `import FIMountKit` in app code |
| **Platform** | macOS 13+ |
| **Swift** | 5.9+ |
Designed for forensic workflows and system-level analysis: DMG, RAW/DD, EWF, AFF4, VMDK, VHD, and split RAW images behind a single `ImageMountingService` API.
## ⚠️ Important: You Must Use a Local Package — Not the Xcode URL Flow
**Do not add FIMountKit via Xcode's "Add Package Dependencies" dialog or via a `url:` reference in `Package.swift`.**
FIMountKit's submodules (`libewf-spm`, `libvmdk-spm`, `libvhdi-spm`) use
`unsafeFlags` in their `Package.swift` to link pre-built static libraries.
Swift Package Manager **silently blocks `unsafeFlags` for remote packages**,
which causes the build to fail with cryptic linker errors.
The only supported way to use FIMountKit is to **clone it locally** and add it
as a **local package** in Xcode. This is a one-time setup step.
## Adding FIMountKit to Your App
### Step 1 — Clone with submodules
Always clone with `--recurse-submodules`. The bundled forensic libraries
(`libewf`, `libvmdk`, `libvhdi`, `libcaff4`) are git submodules and will
not be present without this flag.
git clone --recurse-submodules https://github.com/saadtahir-dev/FIMountKit.git
If you already cloned without the flag:
git submodule update --init --recursive
### Step 2 — Add as a local package in Xcode
1. Open your app project in Xcode
2. Go to **File → Add Package Dependencies**
3. Click **Add Local...** (bottom-left of the dialog)
4. Navigate to the folder where you cloned FIMountKit and select it
5. Click **Add Package**
6. In the target dependency sheet, select **FIMountKit** and click **Add Package**
### Step 3 — Import and use
import FIMountKit
## Keeping FIMountKit Up to Date
Since FIMountKit is a local package, updates are managed via git:
cd /path/to/FIMountKit
git pull
git submodule update --recursive
Then in Xcode: **File → Packages → Reset Package Caches** to pick up changes.
## Features
- Plug-and-play SwiftPM library (`FIMountKit` product)
- Extension-based format detection (`ImageFormatDetector`)
- Native macOS attach via `hdiutil` for block-device presentation
- EWF / VMDK / VHD via bundled FUSE tools (`ewfmount`, `vmdkmount`, `vhdimount`) — no Homebrew tool install
- AFF4 via in-process **Libcaff4** reads (no CLI, no FUSE)
- Split RAW merge (chunked, memory-safe) before attach
- Structured logging with per-mount correlation IDs
- `TemporaryResource` tracking and LIFO cleanup on unmount
- Safe for concurrent use (`async`/`await`, TaskGroup-friendly mounters)
- Injectable mounter list for testing or partial format support
## Supported formats
| Category | Extensions | Pipeline |
|---|---|---|
| Apple disk images | `.dmg`, `.sparseimage`, `.sparsebundle` | `hdiutil attach` |
| Raw images | `.raw`, `.dd` | `hdiutil attach` (`CRawDiskImage`) |
| EWF (EnCase) | `.e01`, `.ex01`, `.s01` | `ewfmount` (FUSE) → `ewf1` → `hdiutil` |
| AFF4 | `.aff4` | **Libcaff4** full extract → temp `.img` → `hdiutil` |
| VMware VMDK | `.vmdk` | `vmdkmount` (FUSE) → `vmdk1` → `hdiutil` |
| Microsoft VHD | `.vhd` | `vhdimount` (FUSE) → `vhdi1` → `hdiutil` |
| Split RAW | `.001`, `.002`, … | merge segments → `hdiutil` |
**Detection vs merge:** `ImageFormatDetector` also maps `.000` and `.00001` to split RAW, but `SplitRawMerger` only merges sets that **start at `.001`**. Use a `.001` first segment for reliable split mounts.
**Not supported:** `.vhdx`, `.iso`, `.img` (unmapped alias), and formats outside the table above.
## How it works
Input URL
↓
ImageFormatDetector (file extension → ImageType)
↓
ImageMountingService (path validation, logging, mounter selection)
↓
ImageMounter (per format)
↓
├─ DMG / RAW / sparse → ProcessExecutor → hdiutil
├─ EWF / VMDK / VHD → bundled *mount (FUSE) → RawImageMounter → hdiutil
├─ AFF4 → Libcaff4 AFF4Image → temp file → RawImageMounter → hdiutil
└─ Split RAW → SplitRawMerger → RawImageMounter → hdiutil
↓
MountResult (mount points, devices, temporaryResources)
Most non-native formats are **two-stage**: produce or expose a flat raw image, then reuse `RawImageMounter`.
## Requirements
| Requirement | Needed for |
|---|---|
| macOS 13+ | Package platform (`Package.swift`) |
| Swift 5.9+ / Xcode 15+ | Build |
| `hdiutil` (system) | All mounts that attach a volume |
| [macFUSE](https://osxfuse.github.io/) 4.x or 5.x | **EWF, VMDK, VHD only** (FUSE tools) |
AFF4, DMG, RAW, and split RAW do **not** require macFUSE. AFF4 **does** require enough disk space to hold a full extracted copy of the image.
## Dependencies
FIMountKit bundles four forensic libraries as **git submodules** under `Dependencies/`. They are resolved automatically when cloning with `--recurse-submodules`.
| Submodule | Path | Repo |
|---|---|---|
| `libewf-spm` | `Dependencies/libewf-spm` | [github.com/saadtahir-dev/libewf-spm](https://github.com/saadtahir-dev/libewf-spm) |
| `libvmdk-spm` | `Dependencies/libvmdk-spm` | [github.com/saadtahir-dev/libvmdk-spm](https://github.com/saadtahir-dev/libvmdk-spm) |
| `libvhdi-spm` | `Dependencies/libvhdi-spm` | [github.com/saadtahir-dev/libvhdi-spm](https://github.com/saadtahir-dev/libvhdi-spm) |
| `libcaff4-spm` | `Dependencies/libcaff4-spm` | [github.com/saadtahir-dev/libcaff4-spm](https://github.com/saadtahir-dev/libcaff4-spm) |
Each submodule ships **universal fat static archives** (arm64 + x86_64) and **bundled CLI tools** in SPM resource bundles. OpenSSL/zlib deps are linked statically or via the system where applicable.
**Dependency graph:**
FIMountKit
├── libewf-spm
│ ├── CLibEWF / CLibEWFFuse (static libs)
│ ├── CLibEWFResources (bin: ewfmount, …)
│ └── libewf (Swift: EWFToolLocator)
├── libcaff4-spm
│ ├── Ccaff4 (static libaff4 + headers)
│ └── Libcaff4 (Swift: AFF4Image)
├── libvmdk-spm
│ ├── CLibVMDK (static libs)
│ ├── CLibVMDKResources (bin: vmdkmount, vmdkinfo)
│ └── LibVMDK (Swift: VMDKToolLocator)
└── libvhdi-spm
├── CLibVHDI (static libs)
├── CLibVHDIResources (bin: vhdimount, vhdiinfo)
└── LibVHDI (Swift: VHDIToolLocator)
## Usage
### Default service
import FIMountKit
let service = ImageMountingService(
log: { message, level, component in
print("[\(component.rawValue)] [\(level.rawValue)] \(message)")
}
)
let result = try await service.mount(
url: URL(fileURLWithPath: "/path/to/image.vmdk")
)
for mountPoint in result.mountPointURLs {
print(mountPoint.path)
}
try await service.unmount(result)
### Mount options
let options = MountOptions(
workspaceDirectory: URL(fileURLWithPath: "/tmp/fimountkit-workspace"),
volumeMountPoint: URL(fileURLWithPath: "/Volumes/CaseImage")
)
let result = try await service.mount(
url: URL(fileURLWithPath: "/path/to/image.e01"),
options: options
)
| Option | Purpose |
|---|---|
| `workspaceDirectory` | FUSE mount dirs (EWF/VMDK/VHD), optional split-merge output parent |
| `volumeMountPoint` | Passed to `hdiutil -mountpoint` (must be empty or creatable) |
### Custom mounter set
let service = ImageMountingService(
mounters: [
DMGImageMounter(),
RawImageMounter(),
EWFImageMounter(),
]
)
Default factory (`ImageMounterFactory.makeDefaultMounters()`): DMG, Raw, AFF4, EWF, VMDK, VHDI, SplitRaw — in that order. The service picks the **first** mounter whose `supportedTypes` contains the detected type.
## Tool resolution
`ProcessExecutor` always receives an **absolute executable path**; it does not search `PATH`.
| Component | Tool resolution |
|---|---|
| `DMGImageMounter` | `SystemToolLocator` → `hdiutil` |
| `RawImageMounter` | `SystemToolLocator` → `hdiutil` |
| `EWFMountManager` | `EWFToolLocator.bundledToolPath("ewfmount")` or system fallback |
| `VMDKMountManager` | `VMDKToolLocator.vmdkmount` or system fallback |
| `VHDIMountManager` | `VHDIToolLocator.vhdimount` or system fallback |
| `AFF4ImageMounter` | **Libcaff4** in-process (`AFF4Image`) — no bundled CLI |
| `SplitRawMerger` | Pure Swift / Foundation I/O |
## Public API surface
| Type | Role |
|---|---|
| `ImageMountingService` | `mount(url:options:)`, `unmount(_:)` |
| `ImageFormatDetector` | Extension → `ImageType` |
| `ImageMounter` | Per-format mount/unmount protocol |
| `MountResult` | `sourceURL`, `type`, `deviceIdentifiers`, `mountPointURLs`, `temporaryResources`, `metadata` |
| `MountOptions` | Workspace and volume mount point |
| `MountError` | Typed failures (`unsupportedType`, `detectionFailed`, …) |
| `ImageMounterFactory` | Default mounter list |
| `*ImageMounter` | `DMG`, `Raw`, `AFF4`, `EWF`, `VMDK`, `VHDI`, `SplitRaw` |
## Resource management
- FUSE mount directories, merged split RAW files, and AFF4 temp extracts are recorded in `MountResult.temporaryResources`.
- Unmount runs format-specific detach, then removes temporary artifacts in **reverse order**.
- On mount failure after partial setup, mounters attempt cleanup (e.g. tear down FUSE if `hdiutil` fails).
## Source layout
Sources/ImageMounter/
├── ImageMountingService.swift
├── Detection/ImageFormatDetector.swift
├── Core/ ImageType, MountResult, MountError, MountOptions
├── Mounters/
│ ├── DMG/ DMGImageMounter
│ ├── RAW/ RawImageMounter
│ ├── EWF/ EWFImageMounter, EWFMountManager
│ ├── AFF4/ AFF4ImageMounter
│ ├── VMDK/ VMDKImageMounter, VMDKMountManager
│ ├── VHD/ VHDIImageMounter, VHDIMountManager
│ ├── SplitRAW/ SplitRawImageMounter, SplitRawMerger
│ └── ImageMounterFactory.swift
├── Infrastructure/ ProcessExecutor, HDIUtil helpers, SystemToolLocator, MountPathValidator
└── Logging/ ImageMounterLogHandler, Logger
Dependencies/
├── libewf-spm/ (git submodule)
├── libvmdk-spm/ (git submodule)
├── libvhdi-spm/ (git submodule)
└── libcaff4-spm/ (git submodule)
## Build & test
git clone --recurse-submodules https://github.com/saadtahir-dev/FIMountKit.git
cd FIMountKit
swift package resolve
swift build
swift test
## Notes
- **macFUSE** must be installed and loaded for EWF, VMDK, and VHD mounts.
- **AFF4** mounts read the entire logical image into a temp file before attach — large images need proportional free disk space and time.
- **Multi-part EWF** (`.e01` + `.e02` + …): open the first segment; libewf / `ewfmount` resolve the set.
- **Multiple partitions** per image are supported where `hdiutil` exposes them.
- Host apps embedding bundled binaries may need **Hardened Runtime** exceptions (e.g. disable library validation) when not using App Sandbox.
## Status
- Core mounting paths implemented for all listed formats
- Extensible `ImageMounter` protocol and injectable service
- Validated on Apple Silicon macOS via `image-mounter-poc` bulk regression UI