libchdman-rs 0.288.0

Rust wrapper around MAME's CHD (Compressed Hunks of Data) core. Supports CD/DVD/HD CHD read+write and ships pre-built static archives via the `prebuilt` feature for fast CI builds.
# libchdman-rs

A Rust wrapper for the official MAME CHD core (`chd_file`).

This crate provides native support for reading, writing, and managing CHD (Compressed Hunks of Data) containers by wrapping the original MAME C++ source code. It ensures 100% feature parity with `chdman` by using the same core logic.

## Features

- **Native CHD Support**: Seek, read, and write at both hunk and byte levels.
- **Metadata Management**: Add, read, and delete CHD metadata.
- **Custom I/O**: Implement the `ChdIo` trait to provide your own I/O backends (e.g., in-memory, network, or custom encrypted filesystems).
- **Asynchronous Compression**: High-level `ChdCompressor` API for creating compressed CHDs from custom data sources.
- **chdman parity**: `hd`, `dvd`, `cd`, and `copy` modules cover `createhd`/`extracthd`, `createdvd`/`extractdvd`, `createcd`/`extractcd`, and `copy` byte-for-byte.
- **Zero Re-implementation**: Directly wraps MAME's `libutil/chd.cpp`.

For the full API surface and a chdman-flag-by-flag mapping see
[`docs/format-modules.md`](docs/format-modules.md) and
[`docs/chdman-mapping.md`](docs/chdman-mapping.md).

## Choosing between crates.io and git

libchdman-rs is published to crates.io in a **slim form** that contains
only the Rust wrapper code. The ~900 MB MAME C++ source is **not**
shipped in the crates.io tarball — published crates can't be that
large, and most consumers don't need to compile MAME from source.

### Use crates.io (recommended for most consumers)

If you don't need to compile MAME from source — and you almost never do
— depend on the crates.io version and enable the `prebuilt` feature:

```toml
[dependencies]
libchdman-rs = { version = "0.288", features = ["prebuilt"] }
```

The `prebuilt` feature downloads a pre-built static archive matching
your target triple from this crate's GitHub Releases. See the
["Pre-built static archives"](#pre-built-static-archives-faster-ci-builds)
section below for supported triples, glibc floors, and escape hatches.

Without `features = ["prebuilt"]`, the build script aborts with an
actionable message — the crates.io tarball has no C++ source for a
local build to consume.

### Use the git dependency (for source builds)

If you need to compile MAME's C++ from source — debugging the wrapped
C++, custom MAME patches, or a target triple not covered by the
prebuilt matrix — depend on the git repo:

```toml
[dependencies]
libchdman-rs = { git = "https://github.com/danifunker/libchdman-rs", tag = "v0.288.0" }
```

The git dependency includes the full vendored MAME source tree
(~1 GB), so cold builds are slow.

### Summary

| Need                                  | Use this                          |
|---------------------------------------|-----------------------------------|
| Fast CI, prebuilt target              | crates.io + `prebuilt`            |
| Custom MAME modifications             | git + source build                |
| Unusual target not in prebuilt matrix | git + source build                |
| Reproducible historic builds          | git, pinned by tag                |

## Usage

### Opening a CHD

```rust
use libchdman_rs::Chd;

let chd = Chd::open("game.chd", false, None).expect("Failed to open CHD");
let info = chd.info().expect("info");
println!("Version: {}, logical bytes: {}", info.version, info.logical_bytes);
println!("CD: {}, DVD: {}, HD: {}", info.is_cd, info.is_dvd, info.is_hd);
```

### Creating a hard-disk CHD

```rust
use libchdman_rs::hd::{create_from_path, HdCreateOptions};
use std::path::Path;

create_from_path(
    Path::new("disk.img"),
    Path::new("disk.chd"),
    HdCreateOptions::default(),
    &mut |p| eprintln!("{}/{}", p.bytes_done, p.bytes_total),
    &|| false,
)?;
```

### Creating a CD CHD from a CUE

```rust
use libchdman_rs::cd::{create_from_cue, CdCreateOptions};
use libchdman_rs::parse_codec_spec;
use std::path::Path;

create_from_cue(
    Path::new("game.cue"),
    Path::new("game.chd"),
    CdCreateOptions {
        codecs: parse_codec_spec("cdlz,cdfl,cdzl")?,
        ..CdCreateOptions::default()
    },
    &mut |_| {},
    &|| false,
)?;
```

### Reading a CD CHD as a cooked ISO stream

For MODE1 / MODE1_RAW CD tracks, `CdCookedReader` exposes the 2048-byte/sector
user data as a `Read + Seek` stream — useful for browsing the ISO9660 / UDF
filesystem inside a CD CHD without extracting to a temp file first. Length is
`track.frames * 2048`; sync, header, and ECC bytes are stripped by MAME
regardless of whether the CHD stored the track raw or cooked.

```rust
use libchdman_rs::cd::CdCookedReader;
use libchdman_rs::Chd;
use std::io::{Read, Seek, SeekFrom};

let chd = Chd::open("game.chd", false, None)?;
let mut reader = CdCookedReader::open(chd)?;

// Jump to the ISO9660 Primary Volume Descriptor (LBA 16, byte 16 * 2048).
reader.seek(SeekFrom::Start(16 * 2048))?;
let mut pvd = [0u8; 2048];
reader.read_exact(&mut pvd)?;
assert_eq!(&pvd[1..6], b"CD001");

// Hand `reader` to any ISO9660 / UDF parser that wants Read + Seek.
let total_bytes = reader.len();
```

For multi-track CHDs (PSX / Saturn / mixed-mode PC CDs: one data track plus one
or more audio tracks), use `CdCookedReader::open_track(chd, track_index)` to
pick which track to expose. Position 0 corresponds to the start of the selected
track's user data, not the start of the CHD. The track must be MODE1 / MODE1_RAW;
audio and Mode 2 variants return `Err(ChdError::UnsupportedFormat)`.

```rust
// PSX game: track 0 is data, tracks 1..N are audio.
let chd = Chd::open("psx-game.chd", false, None)?;
let data_reader = CdCookedReader::open_track(chd, 0)?;
```

`CdCookedReader::open` (no `_track`) returns `Err(ChdError::UnsupportedFormat)`
for multi-track CHDs as a back-compat guard; pick a track explicitly with
`open_track` instead. Out-of-range `track_index` returns `Err(ChdError::InvalidData)`.
Use `Chd::info().is_cd` and `cd::list_tracks` to inspect the TOC before opening.
`into_inner()` recovers the owned `Chd` if you need it back.

### Runtime hard-disk writes (MAME-style)

`HdImage` is a block-device view over a hard-disk CHD — the same surface
MAME's `harddisk_image_device` exposes to a running emulated machine.
Writes to a `HdImage` go straight back into the CHD via `write_bytes`,
which is exactly what MAME does when an emulated guest (e.g. a Macintosh
LC) writes to its CHD hard disk.

For an uncompressed CHD, open it writeable in place:

```rust
use libchdman_rs::hd::HdImage;
use std::path::Path;

let mut img = HdImage::open(Path::new("mac_lc.chd"))?;
let ss = img.sector_size() as usize;

let mut buf = vec![0u8; ss];
img.read_sector(0, &mut buf)?;       // read MBR / boot sector
buf[510] = 0x55;
buf[511] = 0xAA;
img.write_sector(0, &buf)?;          // persisted on drop
```

For a **compressed** CHD, MAME's runtime strategy is to keep the parent
CHD untouched and route every write into a fresh uncompressed *diff*
child. `HdImage::open_with_diff` does the same:

```rust
use libchdman_rs::hd::HdImage;
use std::path::Path;

// Parent stays read-only and untouched. Diff is uncompressed and
// linked to the parent by SHA-1.
let mut img = HdImage::open_with_diff(
    Path::new("mac_lc.chd"),       // compressed parent
    Path::new("mac_lc.diff.chd"),  // freshly created diff
)?;
img.write_sector(42, &[0xAB; 512])?;

// Later, re-attach to the existing diff:
let img = HdImage::reopen_diff(
    Path::new("mac_lc.chd"),
    Path::new("mac_lc.diff.chd"),
)?;
```

`Chd::create_with_parent` is the lower-level primitive if you want to
build the diff yourself (it wraps MAME's
`chd_file::create(filename, logicalbytes, hunkbytes, compression, parent)`
overload). Pass `compression = [0; 4]` for an uncompressed diff, which
is the only configuration that supports per-hunk runtime writes.

**Caveats**

- Writes only work on uncompressed CHDs (or uncompressed diffs against a
  compressed parent). MAME's `write_hunk` / `write_bytes` reject
  compressed targets — this is a CHD-format constraint, not a wrapper
  limitation.
- `HdImage::open_with_diff` fails if the diff path already exists. Use
  `reopen_diff` to re-attach to an existing diff.
- `HdImage` validates `buf.len() == sector_size()` and
  `lba < sector_count()`; both fail with `ChdError::InvalidData` since
  the underlying error enum has no `InvalidParameter` variant.
- The diff inherits logical size, hunk size, and unit size from the
  parent — you can't resize on attach.
- For diff-mode images the `HdImage` owns the parent `Chd` handle
  internally and drops the child first, because MAME's `chd_file`
  stores `m_parent` as a non-owning aliasing `shared_ptr` (`chd.cpp`
  L681,L841). Don't extract the inner `Chd` and outlive the wrapper —
  use `as_chd()` / `as_chd_mut()` to reach lower-level APIs while the
  `HdImage` is in scope.

### Re-compressing an existing CHD

```rust
use libchdman_rs::copy::{copy, CopyOptions};
use libchdman_rs::{CHD_CODEC_LZMA, CHD_CODEC_ZLIB};
use std::path::Path;

copy(
    Path::new("old.chd"),
    Path::new("new.chd"),
    CopyOptions {
        hunk_size: None,
        codecs: [CHD_CODEC_LZMA, CHD_CODEC_ZLIB, 0, 0],
    },
    &mut |_| {},
    &|| false,
)?;
```

### Custom I/O

```rust
use libchdman_rs::{Chd, ChdIo};
use std::io::{Read, Write, Seek, SeekFrom};

struct MyCustomIo { ... }
impl Read for MyCustomIo { ... }
impl Write for MyCustomIo { ... }
impl Seek for MyCustomIo { ... }
// ChdIo is automatically implemented for Read + Write + Seek

let io = MyCustomIo::new();
let chd = Chd::open_custom(io, false, None).expect("Failed to open");
```

## Pre-built static archives (faster CI builds)

libchdman-rs wraps MAME's C++ core via the `cc` crate, which can take
several minutes on cold builds. For CI and other build-time-sensitive
consumers, every tagged release ships pre-built static archives that
skip the C++ compile entirely.

Enable the `prebuilt` feature in your `Cargo.toml`:

```toml
libchdman-rs = { version = "0.288", features = ["prebuilt"] }
```

On `cargo build`, the build script downloads the archive matching your
target triple from this repo's GitHub Releases, verifies its sha256, and
links it statically.

### Supported targets

| Triple                              | Notes                                  |
|-------------------------------------|----------------------------------------|
| `x86_64-unknown-linux-gnu`          | Three glibc floors (see below)         |
| `aarch64-unknown-linux-gnu`         | Three glibc floors                     |
| `x86_64-apple-darwin`               | `MACOSX_DEPLOYMENT_TARGET=10.13`       |
| `aarch64-apple-darwin`              | `MACOSX_DEPLOYMENT_TARGET=10.13`       |
| `x86_64-pc-windows-msvc`            |                                        |
| `i686-pc-windows-msvc`              |                                        |

Targets not in the list (musl, BSD, anything exotic) fall back to source
build automatically when `LIBCHDMAN_PREBUILT_FALLBACK=1` is set; otherwise
the build will fail with a clear message.

### Linux: picking a glibc floor

Linux archives are built against two glibc versions, and you choose
which floor your binary should require:

| `LIBCHDMAN_GLIBC` | glibc floor | Built on Ubuntu | Works on (examples)               |
|-------------------|-------------|-----------------|-----------------------------------|
| `2.35` (default)  | 2.35        | 22.04           | Debian 12, RHEL 9, modern distros |
| `2.39`            | 2.39        | 24.04           | Newest distros only               |

If unset (or `auto`), the build script picks `2.35` — the modern sweet
spot. macOS and Windows builds ignore this variable.

> The previous `2.31` floor (Debian 11 / RHEL 8 era) was dropped when
> GitHub retired the `ubuntu-20.04` runner image. Consumers on older
> distros should fall back to source build with
> `LIBCHDMAN_PREBUILT_FALLBACK=1`.

### Escape hatches

| Env var                          | Effect                                                  |
|----------------------------------|---------------------------------------------------------|
| `LIBCHDMAN_FORCE_SOURCE=1`       | Always compile from source, even with `prebuilt` on     |
| `LIBCHDMAN_PREBUILT_FALLBACK=1`  | Silently fall back to source if the download fails      |

### Caching

The downloaded archive is cached under:

- Linux: `~/.cache/libchdman-rs/<version>/` (or `$XDG_CACHE_HOME/libchdman-rs/<version>/`)
- macOS: `~/Library/Caches/libchdman-rs/<version>/`
- Windows: `%LOCALAPPDATA%\libchdman-rs\<version>\`

`cargo clean` doesn't invalidate the cache; bumping the crate version does.

## Versioning

The crate version matches the MAME release it embeds — for example,
`0.288.0` embeds MAME 0.288. If a wrapper-only fix is needed against the
same MAME version, the patch component is bumped (`0.288.1`, `0.288.2`,
...).

## Testing

`cargo test` runs the lib-only suite (round-trip parity, metadata,
custom I/O). It never invokes the `chdman` binary and has no system
dependencies beyond a C++20 toolchain.

For byte-for-byte parity verification against the upstream `chdman`
tool, enable the `chdman_compat_tests` feature locally:

```bash
cargo test --features chdman_compat_tests
```

This is **dev-local only**. The feature gates tests that shell out to
`chdman` on `$PATH`; CI does not install chdman, so these tests never
run there. Use them on demand during development cycles when you want
to confirm that the output of this crate matches what chdman produces
for the same input.

## Build Requirements

- Rust 1.75+
- C++20 compliant compiler (GCC 10+, Clang 10+, MSVC 2019+)

## License

This crate is licensed under the same terms as MAME (BSD-3-Clause).