Saffron is a pure Java library for reading virtual machine disk images and their contained filesystems — no native dependencies required. It supports 8 disk image formats and 8 filesystem types through a unified, type-safe API built on Java 21 sealed interfaces and pattern matching.
- Java 21 or higher
- Maven 3.6+ (for building)
<dependency>
<groupId>io.spicelabs</groupId>
<artifactId>saffron</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>implementation 'io.spicelabs:saffron:0.1.0-SNAPSHOT'| Format | Extensions | Platforms |
|---|---|---|
| QCOW2 | .qcow2 |
QEMU, KVM, libvirt |
| VMDK | .vmdk |
VMware |
| VHD | .vhd |
Hyper-V (legacy), VirtualBox |
| VHDX | .vhdx |
Hyper-V |
| VDI | .vdi |
VirtualBox |
| Raw | .img, .raw |
All platforms |
| GCP | .tar.gz |
Google Cloud |
| AMI | .ami |
Amazon Web Services |
| Filesystem | OS | Notes |
|---|---|---|
| ext4 | Linux | Most common Linux filesystem |
| XFS | Linux | Default on RHEL/CentOS |
| Btrfs | Linux | Copy-on-write, subvolume support |
| NTFS | Windows | Including v1.2 (NT 4.0) through modern |
| FAT32 | Cross-platform | Including FAT16 |
| exFAT | Cross-platform | Flash storage |
| HFS+ | macOS | Mac OS Extended |
| APFS | macOS | Apple File System |
Saffron also detects and mounts non-disk binary payloads as containers, exposing
named entries such as /payload, /kernel, /dtb, or /ramdisk.
| Format | Identifier | Notes |
|---|---|---|
| Linux kernel | bzImage / zImage / Image / uImage | Extracts kernel payload, initramfs, DTB, certificates |
| FIT / uImage | DTB magic + /images node |
Extracts kernel, ramdisk, fdt |
| Device tree blob | 0xd00dfeed |
Plain DTB exposed as /dtb |
| ELF | 0x7f ELF |
Shared objects and executables |
| Raspberry Pi firmware | start.elf, fixup.dat, bootcode.bin |
Firmware files |
| Android boot | ANDROID! |
boot.img with kernel, ramdisk, second, dtb |
| Compressed single payload | gzip / xz / bzip2 magic | .gz, .xz, .bz2 exposed as /payload |
- Unified API: Open any disk format with
DiskReader.open(path)— format auto-detected from magic bytes - 8 disk formats + 8 filesystems: Comprehensive VM image support
- Streaming reads: Read file contents without loading entire disk images into memory
- SecurityPolicy: Configurable limits for decompression bombs, path depth, symlink cycles, and bidi attacks
- PURL support: Generate standard Package URLs for disk images
- GPT + MBR partition detection: Automatic partition table parsing
- LVM2 support: Detect and mount logical volumes within disk images
- Sealed interfaces + pattern matching: Type-safe API using Java 21 features
- Null-safe API: Uses
Optional<T>and@NotNullannotations throughout - Zero native dependencies: Pure Java — runs anywhere Java 21 runs
import io.spicelabs.saffron.DiskReader;
import io.spicelabs.saffron.VirtualDisk;
try (VirtualDisk disk = DiskReader.open(Path.of("server.qcow2"))) {
System.out.println("Format: " + disk.format());
System.out.println("Virtual size: " + disk.virtualSize());
System.out.println("PURL: " + disk.packageUrl());
}import io.spicelabs.saffron.fs.FileSystem;
import io.spicelabs.saffron.fs.FileSystemMount;
try (VirtualDisk disk = DiskReader.open(Path.of("server.vmdk"))) {
FileSystemMount mount = new FileSystemMount();
// Mount all detected filesystems (partitions + LVM volumes)
List<FileSystem> filesystems = mount.mountAll(disk);
for (FileSystem fs : filesystems) {
System.out.println("Type: " + fs.type());
System.out.println("Label: " + fs.label().orElse("(none)"));
}
}import io.spicelabs.saffron.fs.FileSystemEntry;
try (VirtualDisk disk = DiskReader.open(path)) {
FileSystemMount mount = new FileSystemMount();
FileSystem fs = mount.mountLargest(disk);
// Walk all entries depth-first
try (Stream<FileSystemEntry> entries = fs.walk()) {
entries.forEach(entry -> {
System.out.println(entry.basicInfo().path());
});
}
}// Resolve a specific file
Optional<FileSystemEntry> entry = fs.resolve("/etc/hostname");
if (entry.isPresent() && entry.get() instanceof FileSystemEntry.RegularFile file) {
byte[] contents = file.readAllBytes();
System.out.println(new String(contents));
}FileSystem fs = mount.mountLargest(disk);
String info = switch (fs) {
case FileSystem.Ext4FileSystem ext4 -> "ext4 filesystem";
case FileSystem.NtfsFileSystem ntfs -> "NTFS filesystem";
case FileSystem.XfsFileSystem xfs -> "XFS filesystem";
case FileSystem.BtrfsFileSystem btrfs -> "Btrfs filesystem";
case FileSystem.Fat32FileSystem fat -> "FAT32 filesystem";
case FileSystem.ExFatFileSystem exfat -> "exFAT filesystem";
case FileSystem.HfsPlusFileSystem hfs -> "HFS+ filesystem";
case FileSystem.ApfsFileSystem apfs -> "APFS filesystem";
};Saffron includes multiple security protections configurable via SecurityPolicy:
- Decompression bomb protection: Configurable limit on decompressed data size (default 16 GB)
- Symlink depth limiting: Prevents infinite symlink resolution loops (default 40 levels)
- Walk cycle detection: Detects and breaks filesystem traversal cycles
- Path depth limits: Prevents excessively deep directory trees (default 256 levels)
- Bidi/zero-width character rejection: Detects Unicode homoglyph attacks in filenames
- ResourceLimitException: Thrown when any security limit is exceeded
SecurityPolicy policy = SecurityPolicy.builder()
.maxDecompressedSize(4L * 1024 * 1024 * 1024) // 4 GB
.maxSymlinkDepth(20)
.maxPathDepth(128)
.build();
try (VirtualDisk disk = DiskReader.open(path)) {
// SecurityPolicy is applied during filesystem operations
}Install JDK 21+ and Maven 3.6+.
Clone the repo:
git clone https://github.com/spice-labs-inc/saffron.git
cd saffronBuild with Maven:
mvn clean installRun tests only:
mvn testRun tests without coverage checks (faster):
mvn test -PquickGenerate Javadoc:
mvn javadoc:javadocCheck test coverage (report in target/site/jacoco/):
mvn test jacoco:reportSaffron includes a comprehensive corpus of 70 real-world VM images tested against ground truth generated by external tools (libguestfs). These tests verify exact file counts, directory counts, and SHA256 hashes for sampled files.
Corpus tests require a local test-corpus/ directory with disk images and are skipped in CI. See CONTRIBUTING.md for details.
-
Create a GitHub Release Use a tag like
v0.1.0. This triggers GitHub Actions to:- Build the JAR
- Publish to GitHub Packages
- Upload artifacts to Maven Central (automated)
-
Monitor Maven Central (optional) Visit https://central.sonatype.com → Deployments Propagation takes ~40 minutes.
-
Verify the JAR
mvn dependency:get \
-Dartifact=io.spicelabs:saffron:0.1.0Maintained by Spice Labs.
saffron— this librarybaharat— Java library for reading Linux and BSD package filesspice-labs-cli— Spice Labs Surveyor CLI
- QCOW2 Specification
- VMDK Virtual Disk Format
- VHD Specification
- VHDX Format Specification
- VDI Format (VirtualBox)
- ext4 Data Structures
- NTFS Documentation
- XFS Algorithms & Data Structures
- Btrfs Wiki
- FAT Filesystem Specification
- Apple File System Reference
- HFS Plus Volume Format
Licensed under the Apache License 2.0.