Skip to main content
Version: 3.0.0-rc.2

Create ISO 9660 images

hadris-iso writes an image from a hadris::fs::Tree: the same input tree the other Hadris writers take. The writer can emit the primary ISO namespace, Rock Ridge, Joliet, an ISO 9660:1999 enhanced tree, El Torito boot catalogs and hybrid MBR/GPT tables. It writes to any hadris-storage block device, in the sync and Send async APIs.

Dependency​

[dependencies.hadris]
version = "3.0.0-rc.2"
default-features = false
features = ["std", "sync", "iso"]

The default features (std, sync) include the writer. Without std, alloc is enough.

Create a basic image​

use hadris::fs::{Content, Node, Tree};
use hadris::iso::{IsoId, IsoOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut tree = Tree::new();
tree.insert("README.TXT", Node::file(Content::bytes("Hello from Hadris\n")))?;
tree.insert("DOCS/GUIDE.TXT", Node::file(Content::bytes("Getting started\n")))?;

let options = IsoOptions::default()
.with_id(IsoId::Volume, "HADRIS_DEMO")
.with_id(IsoId::Preparer, "HADRIS")
.with_id(IsoId::Application, "MY_APP");

let image = std::fs::File::options()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open("demo.iso")?;
let image = hadris::storage::host::FileDevice::new(image)?;
let report = hadris::iso::sync::write(image, &tree, &options)?;
println!("{} bytes", report.size());
Ok(())
}

with_id sets each descriptor identifier and with_date each descriptor date. Identifiers are stored as given, as xorriso does; one longer than its field fails the plan with Detail::Identifier. insert creates missing parent directories. The output file does not need to be pre-sized: write checks the planned size against the device's max_block_count, and a host file or Vec<u8> grows. hadris::iso::plan returns the same Report without any I/O when a device must be sized first, such as a MemDevice.

The Report also lists where each file went (extents(path), files()) and warnings for what the options could not store, such as permissions without Rock Ridge or a symlink in an image without it.

Choose the name rules​

use hadris::iso::{IsoLevel, IsoOptions, NameCase};

let portable = IsoOptions::default()
.with_level(IsoLevel::L2)
.with_joliet();

let unix = IsoOptions::default()
.with_level(IsoLevel::L3)
.with_name_case(NameCase::Preserve)
.with_iso1999();

IsoLevel is the ECMA-119 interchange level of the primary tree: L1 for 8.3 names, L2 for 31-character names, L3 for multi-extent files above 4 GiB. NameCase::Preserve keeps lowercase letters. with_joliet adds a Joliet tree and with_iso1999 an ISO 9660:1999 tree with long names. Joliet is usually the most interoperable choice for Unicode names.

Preserve POSIX metadata with Rock Ridge​

use hadris::fs::{Content, DateTime, Node, Owner, Permissions, SetAttr, Tree};
use hadris::iso::IsoOptions;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut tree = Tree::new();
let time = DateTime::from_unix_seconds(1_700_000_000)?;
let attrs = SetAttr::new()
.with_permissions(Permissions::new(0o755))
.with_owner(Owner::new(1000, 1000))
.with_modified(time);
let script = Content::bytes("#!/bin/sh\necho hello\n");
tree.insert("run.sh", Node::file(script).with_attrs(attrs))?;
tree.insert("latest", Node::symlink("run.sh"))?;

let options = IsoOptions::default().with_rock_ridge();
let _ = hadris::iso::plan(&tree, &options)?;
Ok(())
}

Rock Ridge stores modes, owners, times, symlinks, device nodes and hard links. Entries without times get the options' time: NoClock::TIME (1980-01-01) by default, so images are reproducible, and with_time sets another, such as hadris::fs::host::source_date_epoch(). The GUIDs of hybrid images derive from the tree's paths, sizes and times together with with_seed, or the time without one. Directories nested deeper than ECMA-119 allows move into a relocation directory, rr_moved by default; IsoOptions::with_relocation picks Relocation::DotRrMoved for .rr_moved or Relocation::Refuse to refuse such trees, and with_preserve chooses which metadata Rock Ridge copies. libarchive and bsdtar read relocated directories only from those two names.

Create from a host directory​

use hadris::fs::host::{self, OnError, TreeOptions};

let options = TreeOptions::new().with_on_error(OnError::Skip);
let (tree, skipped) = host::read_tree("image-root", &options)?;
for error in skipped {
eprintln!("skipped: {error}");
}
# Ok::<(), hadris::fs::PathError>(())

host::read_tree records host paths, not contents, so the writer streams each file once while writing the image. It keeps modes, owners, times, symlinks, device nodes and hard links; TreeOptions also sets the symlink policy, an exclude filter, an owner override and an mtime clamp.

Bootable and hybrid images​

use hadris::fs::Content;
use hadris::iso::{AppendedPartition, BootEntry, ElTorito, Hybrid, IsoOptions};

let esp = Content::bytes(vec![0u8; 1440 * 1024]);
let options = IsoOptions::default()
.with_el_torito(
ElTorito::new()
.with_entry(BootEntry::bios("boot/bios.img").with_load_size(4))
.with_entry(BootEntry::uefi_appended(0)),
)
.with_hybrid(Hybrid::gpt_hybrid_mbr().with_appended(AppendedPartition::esp(esp)));

BootEntry::bios and BootEntry::uefi boot images that are paths in the tree. Hybrid::with_appended stores a partition after the files and lists it in the GPT; BootEntry::uefi_appended boots it, so the EFI system partition is stored once. Hybrid::mbr and Hybrid::gpt write a single table. ElTorito::with_catalog_path makes the boot catalog visible as a file. For a full example, run:

cargo run -p hadris-iso --example create_bootable_iso -- bootable.iso

Use hadris::udf::sync::write_bridge instead when the same payload must be visible through both ISO 9660 and UDF namespaces.

Add to an existing image​

hadris::iso::sync::Session::open reads an image into a tree whose files point at their existing extents. Change tree_mut(), then write it with SessionMode::Append (a new session after the old one) or SessionMode::Rewrite (new directories in place). Unchanged files are not copied.

Validate the result​

xorriso -indev demo.iso -toc
7z l demo.iso
hadris iso check --strict demo.iso

Treat external validation as part of release testing, especially for bootable, enhanced-namespace, and hybrid images.