Switch libchdr to chd-rs

This commit is contained in:
CasualPokePlayer 2024-05-02 22:14:56 -07:00
parent 186a4a16f4
commit 6cab4a4f99
14 changed files with 1262 additions and 311 deletions

BIN
Assets/dll/chd_capi.dll Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
/target/

243
ExternalProjects/libchd-rs-capi/Cargo.lock generated Normal file
View File

@ -0,0 +1,243 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "autocfg"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "bitreader"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd859c9d97f7c468252795b35aeccc412bdbb1e90ee6969c4fa6328272eaeff"
dependencies = [
"cfg-if",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chd"
version = "0.3.1"
source = "git+https://github.com/SnowflakePowered/chd-rs#891b4296dccbe5be383284fd22b7a015956d43e6"
dependencies = [
"arrayvec",
"bitreader",
"byteorder",
"claxon",
"crc",
"flate2",
"lzma-rs-perf-exp",
"num-derive",
"num-traits",
"ruzstd",
"take_mut",
"text_io",
]
[[package]]
name = "chd-capi"
version = "0.3.1"
dependencies = [
"chd",
]
[[package]]
name = "claxon"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
]
[[package]]
name = "derive_more"
version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "flate2"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "lzma-rs-perf-exp"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38435c1305548bb408c98242841c3cf161246323e72a3e4433787f3c05bf18ee"
dependencies = [
"byteorder",
"crc",
]
[[package]]
name = "miniz_oxide"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [
"adler",
]
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]]
name = "num-traits"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
]
[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ruzstd"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5174a470eeb535a721ae9fdd6e291c2411a906b96592182d05217591d5c5cf7b"
dependencies = [
"byteorder",
"derive_more",
"twox-hash",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "take_mut"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
[[package]]
name = "text_io"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f0c8eb2ad70c12a6a69508f499b3051c924f4b1cfeae85bfad96e6bc5bba46"
[[package]]
name = "twox-hash"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"static_assertions",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

View File

@ -0,0 +1,23 @@
[package]
name = "chd-capi"
version = "0.3.1"
edition = "2021"
authors = ["Ronny Chan <ronny@ronnychan.ca>"]
description = "libchdr-compatible C API for a Rust implementation of the CHD File Format"
license = "BSD-3-Clause"
repository = "https://github.com/SnowflakePowered/chd-rs"
readme = "README.md"
categories = ["emulators", "compression", "encoding"]
keywords = ["mame", "chd", "decompression"]
[lib]
doctest = false
crate-type = ["cdylib"]
[features]
default = []
chd_precache = []
verify_block_crc = ["chd/verify_block_crc"]
[dependencies]
chd = { version = "0.3.1", git = "https://github.com/SnowflakePowered/chd-rs" }

View File

@ -0,0 +1,43 @@
# `chd-capi`
⚠️*The C API has not been heavily tested. Use at your own risk.* ⚠️
chd-rs provides a C API compatible with [chd.h](https://github.com/rtissera/libchdr/blob/6eeb6abc4adc094d489c8ba8cafdcff9ff61251b/include/libchdr/chd.h).
ABI compatibility is detailed below but is untested when compiling as a dynamic library. The intended consumption for this crate is not via cargo, but by vendoring
the [sources of the C API](https://github.com/SnowflakePowered/chd-rs/tree/master/chd-rs-capi) in tree, along with a compatible `libchdcorefile` implementation
for your platform.
## Features
### `verify_block_crc`
Enables the `verify_block_crc` of the `chd` crate to verify decompressed CHD hunks with their internal hash.
### `chd_core_file`
Enables `core_file*` and the`chd_open_file`, and `chd_core_file` APIs. This feature requires a `libchdcorefile` implementation,
or the default POSIX compatible implementation (where `core_file*` is `FILE*`) will be used.
Note that by default, `core_file*` is not an opaque pointer and is a C `FILE*` stream. This allows the underlying
file pointer to be changed unsafely beneath the memory safety guarantees of chd-rs. We strongly encourage using
`chd_open` instead of `chd_open_file`.
If you need `core_file*` support, chd-capi should have the `chd_core_file` feature enabled, which will wrap
`FILE*` to be usable in Rust with a lightweight wrapper in `libchdcorefile`. If the default implementation
is not suitable, you may need to implement `libchdcorefile` yourself. The `chd_core_file` feature requires
CMake and Clang to be installed.
### `chd_virtio`
Enables the [virtual I/O](https://github.com/rtissera/libchdr/pull/78) functions `chd_open_core_file`.
Because this C API requires `core_file` to be an opaque pointer, there is no difference between `chd_open_file` and
`chd_open_core_file` unlike libchdr, and `chd_open_core_file` is simply an alias for `chd_open_file`. All functions that
take `core_file*` require a `libchdcorefile` implementation.
### `chd_precache`
Enables precaching of the underlying file into memory with the `chd_precache_progress` and `chd_precache` functions.
## ABI compatibility
chd-rs makes the following ABI-compatibility guarantees compared to libchdr when compiled statically.
* `chd_error` is ABI and API-compatible with [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L258)
* `chd_header` is ABI and API-compatible [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L302)
* `chd_file *` is an opaque pointer. It is **not layout compatible** with [chd.c](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/src/libchdr_chd.c#L265)
* The layout of `core_file *` is user-defined when the `chd_core_file` feature is enabled.
* Freeing any pointer returned by chd-rs with `free` is undefined behaviour. The exception are `chd_file *` pointers which can be safely freed with `chd_close`.

View File

@ -0,0 +1,3 @@
@cargo b --release
@copy target\release\chd_capi.dll ..\..\Assets\dll
@copy target\release\chd_capi.dll ..\..\output\dll

View File

@ -0,0 +1,9 @@
#!/bin/sh
if [ -z "$BIZHAWKBUILD_HOME" ]; then export BIZHAWKBUILD_HOME="$(realpath "$(dirname "$0")/../..")"; fi
cargo b --release
cp target/release/libchd_capi.so "$BIZHAWKBUILD_HOME/Assets/dll"
if [ -e "$BIZHAWKBUILD_HOME/output" ]; then
cp target/release/libchd_capi.so "$BIZHAWKBUILD_HOME/output/dll"
fi

View File

@ -0,0 +1,168 @@
use crate::chd_file;
use chd::header::{Header, HeaderV1, HeaderV3, HeaderV4, HeaderV5};
use chd::map::Map;
use std::mem;
pub const CHD_MD5_BYTES: usize = 16;
pub const CHD_SHA1_BYTES: usize = 20;
#[repr(C)]
#[allow(non_camel_case_types)]
/// libchdr-compatible CHD header struct.
/// This struct is ABI-compatible with [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L302)
pub struct chd_header {
length: u32,
version: u32,
flags: u32,
compression: [u32; 4],
hunkbytes: u32,
totalhunks: u32,
logicalbytes: u64,
metaoffset: u64,
mapoffset: u64,
md5: [u8; CHD_MD5_BYTES],
parentmd5: [u8; CHD_MD5_BYTES],
sha1: [u8; CHD_SHA1_BYTES],
rawsha1: [u8; CHD_SHA1_BYTES],
parentsha1: [u8; CHD_SHA1_BYTES],
unitbytes: u32,
unitcount: u64,
hunkcount: u32,
mapentrybytes: u32,
rawmap: *mut u8,
obsolete_cylinders: u32,
obsolete_sectors: u32,
obsolete_heads: u32,
obsolete_hunksize: u32,
}
impl From<&HeaderV1> for chd_header {
fn from(header: &HeaderV1) -> Self {
chd_header {
length: header.length,
version: header.version as u32,
flags: header.flags,
compression: [header.compression, 0, 0, 0],
hunkbytes: header.hunk_bytes,
totalhunks: header.total_hunks,
logicalbytes: header.logical_bytes,
metaoffset: 0,
mapoffset: 0,
md5: header.md5,
parentmd5: header.parent_md5,
sha1: [0u8; CHD_SHA1_BYTES],
rawsha1: [0u8; CHD_SHA1_BYTES],
parentsha1: [0u8; CHD_SHA1_BYTES],
unitbytes: header.unit_bytes,
unitcount: header.unit_count,
hunkcount: header.total_hunks,
mapentrybytes: 0,
rawmap: std::ptr::null_mut(),
obsolete_cylinders: header.cylinders,
obsolete_sectors: header.sectors,
obsolete_heads: header.heads,
obsolete_hunksize: header.hunk_size,
}
}
}
impl From<&HeaderV3> for chd_header {
fn from(header: &HeaderV3) -> Self {
chd_header {
length: header.length,
version: header.version as u32,
flags: header.flags,
compression: [header.compression, 0, 0, 0],
hunkbytes: header.hunk_bytes,
totalhunks: header.total_hunks,
logicalbytes: header.logical_bytes,
metaoffset: header.meta_offset,
mapoffset: 0,
md5: header.md5,
parentmd5: header.parent_md5,
sha1: header.sha1,
rawsha1: [0u8; CHD_SHA1_BYTES],
parentsha1: header.parent_sha1,
unitbytes: header.unit_bytes,
unitcount: header.unit_count,
hunkcount: header.total_hunks,
mapentrybytes: 0,
rawmap: std::ptr::null_mut(),
obsolete_cylinders: 0,
obsolete_sectors: 0,
obsolete_heads: 0,
obsolete_hunksize: 0,
}
}
}
impl From<&HeaderV4> for chd_header {
fn from(header: &HeaderV4) -> Self {
chd_header {
length: header.length,
version: header.version as u32,
flags: header.flags,
compression: [header.compression, 0, 0, 0],
hunkbytes: header.hunk_bytes,
totalhunks: header.total_hunks,
logicalbytes: header.logical_bytes,
metaoffset: header.meta_offset,
mapoffset: 0,
md5: [0u8; CHD_MD5_BYTES],
parentmd5: [0u8; CHD_MD5_BYTES],
sha1: header.sha1,
rawsha1: header.raw_sha1,
parentsha1: header.parent_sha1,
unitbytes: header.unit_bytes,
unitcount: header.unit_count,
hunkcount: header.total_hunks,
mapentrybytes: 0,
rawmap: std::ptr::null_mut(),
obsolete_cylinders: 0,
obsolete_sectors: 0,
obsolete_heads: 0,
obsolete_hunksize: 0,
}
}
}
pub(crate) fn get_v5_header(chd: &chd_file) -> chd_header {
let header: HeaderV5 = match chd.header() {
Header::V5Header(h) => h.clone(),
_ => unreachable!(),
};
let mut map_data: Vec<u8> = match chd.map() {
Map::V5(map) => map.into(),
_ => unreachable!(),
};
let version = header.version;
let map_ptr = map_data.as_mut_ptr();
mem::forget(map_data);
chd_header {
length: header.length,
version: version as u32,
// libchdr just reads garbage for V5 flags, we will give it as 0.
flags: 0,
compression: header.compression,
hunkbytes: header.hunk_bytes,
totalhunks: header.hunk_count,
logicalbytes: header.logical_bytes,
metaoffset: header.meta_offset,
mapoffset: header.map_offset,
md5: [0u8; CHD_MD5_BYTES],
parentmd5: [0u8; CHD_MD5_BYTES],
sha1: header.sha1,
rawsha1: header.raw_sha1,
parentsha1: header.parent_sha1,
unitbytes: header.unit_bytes,
unitcount: header.unit_count,
hunkcount: header.hunk_count,
mapentrybytes: header.map_entry_bytes,
rawmap: map_ptr,
obsolete_cylinders: 0,
obsolete_sectors: 0,
obsolete_heads: 0,
obsolete_hunksize: 0,
}
}

View File

@ -0,0 +1,595 @@
#![cfg_attr(docsrs, feature(doc_cfg, doc_cfg_hide))]
#![deny(unsafe_op_in_unsafe_fn)]
//! A (mostly) [libchdr](https://github.com/rtissera/libchdr) compatible C-API for [chd-rs](https://crates.io/crates/chd).
//!
//! For Rust consumers, consider using [chd-rs](https://crates.io/crates/chd) instead.
//!
//! The best way to integrate chd-rs in your C or C++ project is to instead vendor the [sources](https://github.com/SnowflakePowered/chd-rs) directly
//! into your project, with a compatible implementation of [libchdcorefile](https://github.com/SnowflakePowered/chd-rs/tree/master/chd-rs-capi/libchdcorefile)
//! for your platform as required.
//!
//! ## ABI compatibility with libchdr
//!
//! chd-rs-capi makes the following ABI-compatibility guarantees compared to libchdr when compiled statically.
//! * `chd_error` is ABI and API-compatible with [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L258)
//! * `chd_header` is ABI and API-compatible [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L302)
//! * `chd_file *` is an opaque pointer. It is **not layout compatible** with [chd.c](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/src/libchdr_chd.c#L265)
//! * The layout of `core_file *` is user-defined when the `chd_core_file` feature is enabled.
//! * Freeing any pointer returned by chd-rs with `free` is undefined behaviour. The exception are `chd_file *` pointers which can be safely freed with `chd_close`.
extern crate core;
mod header;
#[cfg(feature = "chd_core_file")]
mod chdcorefile;
#[cfg(feature = "chd_core_file")]
#[allow(non_camel_case_types)]
#[allow(unused)]
mod chdcorefile_sys;
use crate::header::chd_header;
use chd::header::Header;
use chd::metadata::{KnownMetadata, Metadata, MetadataTag};
pub use chd::Error as chd_error;
use chd::{Chd, Error};
use std::any::Any;
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::{BufReader, Cursor, Read, Seek};
use std::mem::MaybeUninit;
use std::os::raw::{c_char, c_int, c_void};
use std::path::Path;
use std::slice;
/// Open a CHD for reading.
pub const CHD_OPEN_READ: i32 = 1;
/// Open a CHD for reading and writing. This mode is not supported and will always return an error
/// when passed into a constructor function such as [`chd_open`](crate::chd_open).
pub const CHD_OPEN_READWRITE: i32 = 2;
/// Trait alias for `Read + Seek + Any`.
#[doc(hidden)]
pub trait SeekRead: Any + Read + Seek {
fn as_any(&self) -> &dyn Any;
}
impl<R: Any + Read + Seek> SeekRead for BufReader<R> {
fn as_any(&self) -> &dyn Any {
self
}
}
impl SeekRead for Cursor<Vec<u8>> {
fn as_any(&self) -> &dyn Any {
self
}
}
#[allow(non_camel_case_types)]
/// An opaque type for an opened CHD file.
pub type chd_file = Chd<Box<dyn SeekRead>>;
fn ffi_takeown_chd(chd: *mut chd_file) -> Box<Chd<Box<dyn SeekRead>>> {
unsafe { Box::from_raw(chd) }
}
fn ffi_expose_chd(chd: Box<Chd<Box<dyn SeekRead>>>) -> *mut chd_file {
Box::into_raw(chd)
}
fn ffi_open_chd(
filename: *const c_char,
parent: Option<Box<chd_file>>,
) -> Result<chd_file, chd_error> {
let c_filename = unsafe { CStr::from_ptr(filename) };
let filename = std::str::from_utf8(c_filename.to_bytes())
.map(Path::new)
.map_err(|_| chd_error::InvalidParameter)?;
let file = File::open(filename).map_err(|_| chd_error::FileNotFound)?;
let bufread = Box::new(BufReader::new(file)) as Box<dyn SeekRead>;
Chd::open(bufread, parent)
}
/// Opens a CHD file by file name, with a layout-undefined backing file pointer owned by
/// the library.
///
/// The result of passing an object created by this function into [`chd_core_file`](crate::chd_core_file)
/// is strictly undefined. Instead, all `chd_file*` pointers with provenance from `chd_open` should be
/// closed with [`chd_close`](crate::chd_close).
///
/// # Safety
/// * `filename` is a valid, null-terminated **UTF-8** string.
/// * `parent` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
/// * `out` is aligned and can store a pointer to a `chd_file*`. On success, `out` will point to a valid `chd_file*`.
/// * After this function returns, `parent` is invalid and must not be used, otherwise it will be undefined behaviour. There is no way to retake ownership of `parent`.
#[no_mangle]
pub unsafe extern "C" fn chd_open(
filename: *const c_char,
mode: c_int,
parent: *mut chd_file,
out: *mut *mut chd_file,
) -> chd_error {
// we don't support READWRITE mode
if mode == CHD_OPEN_READWRITE {
return chd_error::FileNotWriteable;
}
let parent = if parent.is_null() {
None
} else {
Some(ffi_takeown_chd(parent))
};
let chd = match ffi_open_chd(filename, parent) {
Ok(chd) => chd,
Err(e) => return e,
};
unsafe { *out = ffi_expose_chd(Box::new(chd)) }
chd_error::None
}
#[no_mangle]
/// Close a CHD file.
///
/// # Safety
/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
/// * If `chd` is `NULL`, this does nothing.
pub unsafe extern "C" fn chd_close(chd: *mut chd_file) {
if !chd.is_null() {
unsafe { drop(Box::from_raw(chd)) }
}
}
#[no_mangle]
/// Returns an error string for the corresponding CHD error.
///
/// # Safety
/// The returned string is leaked and the memory **should not and can not ever** be validly freed.
/// Attempting to free the returned pointer with `free` is **undefined behaviour**.
pub unsafe extern "C" fn chd_error_string(err: chd_error) -> *const c_char {
// SAFETY: This will leak, but this is much safer than
// potentially allowing the C caller to corrupt internal state
// by returning an internal pointer to an interned string.
let err_string = unsafe { CString::new(err.to_string()).unwrap_unchecked() };
err_string.into_raw()
}
fn ffi_chd_get_header(chd: &chd_file) -> chd_header {
match chd.header() {
Header::V5Header(_) => header::get_v5_header(chd),
Header::V1Header(h) | Header::V2Header(h) => h.into(),
Header::V3Header(h) => h.into(),
Header::V4Header(h) => h.into(),
}
}
#[no_mangle]
/// Returns a pointer to the extracted CHD header data.
/// # Safety
/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
/// * If `chd` is `NULL`, returns `NULL`.
/// * The returned pointer is leaked and the memory **should not and can not ever** be validly freed. Attempting to free the returned pointer with `free` is **undefined behaviour**. A non-leaking variant is provided in [`chd_read_header`](crate::chd_read_header).
pub unsafe extern "C" fn chd_get_header(chd: *const chd_file) -> *const chd_header {
match unsafe { chd.as_ref() } {
Some(chd) => {
let header = ffi_chd_get_header(chd);
Box::into_raw(Box::new(header))
}
None => std::ptr::null(),
}
}
#[no_mangle]
/// Read a single hunk from the CHD file.
///
/// # Safety
/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
/// * `buffer` must an aligned pointer to a block of initialized memory of exactly the hunk size for the input `chd_file*` that is valid for both reads and writes. This size can be found with [`chd_get_header`](crate::chd_get_header).
/// * If `chd` is `NULL`, returns `CHDERR_INVALID_PARAMETER`.
pub unsafe extern "C" fn chd_read(
chd: *mut chd_file,
hunknum: u32,
buffer: *mut c_void,
) -> chd_error {
match unsafe { chd.as_mut() } {
None => chd_error::InvalidParameter,
Some(chd) => {
let hunk = chd.hunk(hunknum);
if let Ok(mut hunk) = hunk {
let size = hunk.len();
let mut comp_buf = Vec::new();
// SAFETY: The output buffer *must* be initialized and
// have a length of exactly the hunk size.
let output: &mut [u8] =
unsafe { slice::from_raw_parts_mut(buffer as *mut u8, size) };
let result = hunk.read_hunk_in(&mut comp_buf, output);
match result {
Ok(_) => chd_error::None,
Err(e) => e,
}
} else {
chd_error::HunkOutOfRange
}
}
}
}
fn find_metadata(
chd: &mut chd_file,
search_tag: u32,
mut search_index: u32,
) -> Result<Metadata, Error> {
for entry in chd.metadata_refs() {
if entry.metatag() == search_tag || entry.metatag() == KnownMetadata::Wildcard.metatag() {
if search_index == 0 {
return entry.read(chd.inner());
}
search_index -= 1;
}
}
Err(Error::MetadataNotFound)
}
#[no_mangle]
/// Get indexed metadata of the given search tag and index.
///
/// # Safety
/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
/// * `output` must be an aligned pointer to a block of initialized memory of size exactly `output_len` that is valid for writes.
/// * `result_len` must be either NULL or an aligned pointer to a `uint32_t` that is valid for writes.
/// * `result_tag` must be either NULL or an aligned pointer to a `uint32_t` that is valid for writes.
/// * `result_flags` must be either NULL or an aligned pointer to a `uint8_t` that is valid for writes.
/// * If `chd` is `NULL`, returns `CHDERR_INVALID_PARAMETER`.
pub unsafe extern "C" fn chd_get_metadata(
chd: *mut chd_file,
searchtag: u32,
searchindex: u32,
output: *mut c_void,
output_len: u32,
result_len: *mut u32,
result_tag: *mut u32,
result_flags: *mut u8,
) -> chd_error {
match unsafe { chd.as_mut() } {
Some(chd) => {
let entry = find_metadata(chd, searchtag, searchindex);
match (entry, searchtag) {
(Ok(meta), _) => {
unsafe {
let output_len = std::cmp::min(output_len, meta.value.len() as u32);
std::ptr::copy_nonoverlapping(
meta.value.as_ptr() as *const c_void,
output,
output_len as usize,
);
if !result_tag.is_null() {
result_tag.write(meta.metatag)
}
if !result_len.is_null() {
result_len.write(meta.length)
}
if !result_flags.is_null() {
result_flags.write(meta.flags)
}
}
chd_error::None
}
(Err(_), tag) => unsafe {
if (tag == KnownMetadata::HardDisk.metatag()
|| tag == KnownMetadata::Wildcard.metatag())
&& searchindex == 0
{
let header = chd.header();
if let Header::V1Header(header) = header {
let fake_meta = format!(
"CYLS:{},HEADS:{},SECS:{},BPS:{}",
header.cylinders,
header.heads,
header.sectors,
header.hunk_bytes / header.hunk_size
);
let cstring = CString::from_vec_unchecked(fake_meta.into_bytes());
let bytes = cstring.into_bytes_with_nul();
let len = bytes.len();
let output_len = std::cmp::min(output_len, len as u32);
std::ptr::copy_nonoverlapping(
bytes.as_ptr() as *const c_void,
output,
output_len as usize,
);
if !result_tag.is_null() {
result_tag.write(KnownMetadata::HardDisk.metatag())
}
if !result_len.is_null() {
result_len.write(len as u32)
}
return chd_error::None;
}
}
chd_error::MetadataNotFound
},
}
}
None => chd_error::InvalidParameter,
}
}
#[no_mangle]
/// Set codec internal parameters.
///
/// This function is not supported and always returns `CHDERR_INVALID_PARAMETER`.
pub extern "C" fn chd_codec_config(
_chd: *const chd_file,
_param: i32,
_config: *mut c_void,
) -> chd_error {
chd_error::InvalidParameter
}
#[no_mangle]
/// Read CHD header data from the file into the pointed struct.
///
/// # Safety
/// * `filename` is a valid, null-terminated **UTF-8** string.
/// * `header` is either `NULL`, or an aligned pointer to a possibly uninitialized `chd_header` struct.
/// * If `header` is `NULL`, returns `CHDERR_INVALID_PARAMETER`
pub unsafe extern "C" fn chd_read_header(
filename: *const c_char,
header: *mut MaybeUninit<chd_header>,
) -> chd_error {
let chd = ffi_open_chd(filename, None);
match chd {
Ok(chd) => {
let chd_header = ffi_chd_get_header(&chd);
match unsafe { header.as_mut() } {
None => Error::InvalidParameter,
Some(header) => {
header.write(chd_header);
Error::None
}
}
}
Err(e) => e,
}
}
#[no_mangle]
#[cfg(feature = "chd_core_file")]
#[cfg_attr(docsrs, doc(cfg(chd_core_file)))]
/// Returns the associated `core_file*`.
///
/// This method has different semantics than `chd_core_file` in libchdr.
///
/// The input `chd_file*` will be dropped, and all prior references to
/// to the input `chd_file*` are rendered invalid, with the same semantics as `chd_close`.
///
/// The provenance of the `chd_file*` is important to keep in mind.
///
/// If the input `chd_file*` was opened with [`chd_open`](crate::chd_open), the input `chd_file*` will be closed,
/// and the return value should be considered undefined. For now it is `NULL`, but relying on this
/// behaviour is unstable and may change in the future.
///
/// If the input `chd_file*` was opened with `chd_open_file` and the `chd_core_file` crate feature
/// is enabled, this method will return the same pointer as passed to `chd_input_file`, which may
/// be possible to cast to `FILE*` depending on the implementation of `libchdcorefile` that was
/// linked.
///
/// # Safety
/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
/// * If `chd` is `NULL`, returns `NULL`.
/// * If `chd` has provenance from [`chd_open`](crate::chd_open), the returned pointer is undefined and must not be used.
/// * `chd` is **no longer valid** upon return of this function, and subsequent reuse of the `chd_file*` pointer is **undefined behaviour**.
pub unsafe extern "C" fn chd_core_file(chd: *mut chd_file) -> *mut chdcorefile_sys::core_file {
if chd.is_null() {
return std::ptr::null_mut();
}
let (file, _) = ffi_takeown_chd(chd).into_inner();
let file_ref = file.as_any();
let pointer = match file_ref.downcast_ref::<crate::chdcorefile::CoreFile>() {
None => std::ptr::null_mut(),
Some(file) => file.0,
};
std::mem::forget(file);
pointer
}
#[no_mangle]
#[cfg(feature = "chd_core_file")]
#[cfg_attr(docsrs, doc(cfg(chd_core_file)))]
/// Open an existing CHD file from an opened `core_file` object.
///
/// Ownership is taken of the `core_file*` object and should not be modified until
/// `chd_core_file` is called to retake ownership of the `core_file*`.
///
/// # Safety
/// * `file` is a valid pointer to a `core_file` with respect to the implementation of libchdcorefile that was linked.
/// * `parent` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
/// * `out` is aligned and can store a pointer to a `chd_file*`. On success, `out` will point to a valid `chd_file*`.
/// * Until the returned `chd_file*` in `out` is closed with [`chd_close`](crate::chd_close) or [`chd_core_file`](crate::chd_core_file), external mutation of `file` will result in undefined behaviour.
/// * After this function returns, `parent` is invalid and must not be used, otherwise it will be undefined behaviour. There is no way to retake ownership of `parent`.
pub unsafe extern "C" fn chd_open_file(
file: *mut chdcorefile_sys::core_file,
mode: c_int,
parent: *mut chd_file,
out: *mut *mut chd_file,
) -> chd_error {
// we don't support READWRITE mode
if mode == CHD_OPEN_READWRITE {
return chd_error::FileNotWriteable;
}
let parent = if parent.is_null() {
None
} else {
Some(ffi_takeown_chd(parent))
};
let core_file = Box::new(crate::chdcorefile::CoreFile(file)) as Box<dyn SeekRead>;
let chd = match Chd::open(core_file, parent) {
Ok(chd) => chd,
Err(e) => return e,
};
unsafe { *out = ffi_expose_chd(Box::new(chd)) }
chd_error::None
}
#[no_mangle]
#[cfg(feature = "chd_virtio")]
#[cfg_attr(docsrs, doc(cfg(chd_virtio)))]
/// Open an existing CHD file from an opened `core_file` object.
///
/// Ownership is taken of the `core_file*` object and should not be modified until
/// `chd_core_file` is called to retake ownership of the `core_file*`.
///
/// # Safety
/// * `file` is a valid pointer to a `core_file` with respect to the implementation of libchdcorefile that was linked.
/// * `parent` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
/// * `out` is aligned and can store a pointer to a `chd_file*`. On success, `out` will point to a valid `chd_file*`.
/// * Until the returned `chd_file*` in `out` is closed with [`chd_close`](crate::chd_close) or [`chd_core_file`](crate::chd_core_file), external mutation of `file` will result in undefined behaviour.
/// * After this function returns, `parent` is invalid and must not be used, otherwise it will be undefined behaviour. There is no way to retake ownership of `parent`.
pub unsafe extern "C" fn chd_open_core_file(
file: *mut chdcorefile_sys::core_file,
mode: c_int,
parent: *mut chd_file,
out: *mut *mut chd_file,
) -> chd_error {
chd_open_file(file, mode, parent, out)
}
#[no_mangle]
/// Get the name of a particular codec.
///
/// This method always returns the string "Unknown"
pub extern "C" fn chd_get_codec_name(_codec: u32) -> *const c_char {
b"Unknown\0".as_ptr() as *const c_char
}
#[cfg(feature = "chd_precache")]
use std::io::SeekFrom;
#[cfg(feature = "chd_precache")]
#[cfg_attr(docsrs, doc(cfg(chd_precache)))]
/// The chunk size to read when pre-caching the underlying file stream into memory.
pub const PRECACHE_CHUNK_SIZE: usize = 16 * 1024 * 1024;
#[no_mangle]
#[cfg(feature = "chd_precache")]
#[cfg_attr(docsrs, doc(cfg(chd_precache)))]
/// Precache the underlying file into memory with an optional callback to report progress.
///
/// The underlying stream of the input `chd_file` is swapped with a layout-undefined in-memory stream.
///
/// If the provenance of the original `chd_file` is from [`chd_open`](crate::chd_open), then the underlying
/// stream is safely dropped.
///
/// If instead the underlying stream is a `core_file` opened from [`chd_open_file`](crate::chd_open_file),
/// or [`chd_open_core_file`](crate::chd_open_core_file), then the same semantics of calling [`chd_core_file`](crate::chd_core_file)
/// applies, and ownership of the underlying stream is released to the caller.
///
/// After precaching, the input `chd_file` no longer returns a valid inner stream when passed to [`chd_core_file`](crate::chd_core_file),
/// and should be treated as having the same provenance as being from [`chd_open`](crate::chd_open).
///
/// # Safety
/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
pub unsafe extern "C" fn chd_precache_progress(
chd: *mut chd_file,
progress: Option<unsafe extern "C" fn(pos: usize, total: usize, param: *mut c_void)>,
param: *mut c_void,
) -> chd_error {
let chd_file = if let Some(chd) = unsafe { chd.as_mut() } {
chd
} else {
return chd_error::InvalidParameter;
};
// if the inner is already a cursor over Vec<u8>, then it's already cached.
if chd_file.inner().as_any().is::<Cursor<Vec<u8>>>() {
return chd_error::None;
}
let file = chd_file.inner();
let length = if let Ok(length) = file.seek(SeekFrom::End(0)) {
length as usize
} else {
return chd_error::ReadError;
};
let mut buffer = Vec::new();
if let Err(_) = buffer.try_reserve_exact(length as usize) {
return chd_error::OutOfMemory;
}
let mut done: usize = 0;
let mut last_update_done: usize = 0;
let update_interval: usize = (length + 99) / 100;
if let Err(_) = file.seek(SeekFrom::Start(0)) {
return chd_error::ReadError;
}
while done < length {
let req_count = std::cmp::max(length - done, PRECACHE_CHUNK_SIZE);
// todo: this is kind of sus...
if let Err(_) = file.read_exact(&mut buffer[done..req_count]) {
return chd_error::ReadError;
}
done += req_count;
if let Some(progress) = progress {
if (done - last_update_done) >= update_interval && done != length {
last_update_done = done;
unsafe {
progress(done, length, param);
}
}
}
}
// replace underlying stream of chd_file
let stream = Box::new(Cursor::new(buffer)) as Box<dyn SeekRead>;
// take ownership of the existing chd file
let chd_file = ffi_takeown_chd(chd);
let (_file, parent) = chd_file.into_inner();
let buffered_chd = match Chd::open(stream, parent) {
Err(e) => return e,
Ok(chd) => Box::new(chd),
};
let buffered_chd = ffi_expose_chd(buffered_chd);
unsafe { chd.swap(buffered_chd) };
chd_error::None
}
#[no_mangle]
#[cfg(feature = "chd_precache")]
#[cfg_attr(docsrs, doc(cfg(chd_precache)))]
/// Precache the underlying file into memory.
///
/// The underlying stream of the input `chd_file` is swapped with a layout-undefined in-memory stream.
///
/// If the provenance of the original `chd_file` is from [`chd_open`](crate::chd_open), then the underlying
/// stream is safely dropped.
///
/// If instead the underlying stream is a `core_file` opened from [`chd_open_file`](crate::chd_open_file),
/// or [`chd_open_core_file`](crate::chd_open_core_file), then the same semantics of calling [`chd_core_file`](crate::chd_core_file)
/// applies, and ownership of the underlying stream is released to the caller.
///
/// After precaching, the input `chd_file` no longer returns a valid inner stream when passed to [`chd_core_file`](crate::chd_core_file),
/// and should be treated as having the same provenance as being from [`chd_open`](crate::chd_open).
///
/// # Safety
/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file).
pub unsafe extern "C" fn chd_precache(chd: *mut chd_file) -> chd_error {
chd_precache_progress(chd, None, std::ptr::null_mut())
}

View File

@ -5,16 +5,14 @@ namespace BizHawk.Emulation.DiscSystem
{
internal class Blob_CHD : IBlob
{
private LibChdr.CoreFileStreamWrapper _coreFile;
private IntPtr _chdFile;
private readonly uint _hunkSize;
private readonly byte[] _hunkCache;
private int _currentHunk;
public Blob_CHD(LibChdr.CoreFileStreamWrapper coreFile, IntPtr chdFile, uint hunkSize)
public Blob_CHD(IntPtr chdFile, uint hunkSize)
{
_coreFile = coreFile;
_chdFile = chdFile;
_hunkSize = hunkSize;
_hunkCache = new byte[hunkSize];
@ -25,12 +23,9 @@ namespace BizHawk.Emulation.DiscSystem
{
if (_chdFile != IntPtr.Zero)
{
LibChdr.chd_close(_chdFile);
LibChd.chd_close(_chdFile);
_chdFile = IntPtr.Zero;
}
_coreFile?.Dispose();
_coreFile = null;
}
public int Read(long byte_pos, byte[] buffer, int offset, int count)
@ -41,8 +36,8 @@ namespace BizHawk.Emulation.DiscSystem
var targetHunk = (uint)(byte_pos / _hunkSize);
if (targetHunk != _currentHunk)
{
var err = LibChdr.chd_read(_chdFile, targetHunk, _hunkCache);
if (err != LibChdr.chd_error.CHDERR_NONE)
var err = LibChd.chd_read(_chdFile, targetHunk, _hunkCache);
if (err != LibChd.chd_error.CHDERR_NONE)
{
// shouldn't ever happen in practice, unless something has gone terribly wrong
throw new IOException($"CHD read failed with error {err}");

View File

@ -3,6 +3,7 @@ using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@ -11,7 +12,7 @@ using BizHawk.Emulation.DiscSystem.CUE;
#pragma warning disable BHI1005
// MAME CHD images, using the standard libchdr for reading
// MAME CHD images, using chd-rs for reading
// helpful reference: https://problemkaputt.de/psxspx-cdrom-disk-images-chd-mame.htm
namespace BizHawk.Emulation.DiscSystem
@ -20,24 +21,19 @@ namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// Represents a CHD file.
/// This isn't particularly faithful to the format, but rather it just wraps libchdr's chd_file
/// This isn't particularly faithful to the format, but rather it just wraps a chd_file
/// </summary>
public class CHDFile
{
/// <summary>
/// Wrapper of a C# stream to a chd_core_file
/// </summary>
public LibChdr.CoreFileStreamWrapper CoreFile;
/// <summary>
/// chd_file* to be used for libchdr functions
/// chd_file* to be used for chd_ functions
/// </summary>
public IntPtr ChdFile;
/// <summary>
/// CHD header, interpreted by libchdr
/// CHD header, interpreted by chd-rs
/// </summary>
public LibChdr.chd_header Header;
public LibChd.chd_header Header;
/// <summary>
/// CHD CD metadata for each track
@ -65,12 +61,12 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// Track type
/// </summary>
public LibChdr.chd_track_type TrackType;
public LibChd.chd_track_type TrackType;
/// <summary>
/// Subcode type
/// </summary>
public LibChdr.chd_sub_type SubType;
public LibChd.chd_sub_type SubType;
/// <summary>
/// Size of each sector
@ -104,12 +100,12 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// Pregap track type
/// </summary>
public LibChdr.chd_track_type PregapTrackType;
public LibChd.chd_track_type PregapTrackType;
/// <summary>
/// Pregap subcode type
/// </summary>
public LibChdr.chd_sub_type PregapSubType;
public LibChd.chd_sub_type PregapSubType;
/// <summary>
/// Indicates whether pregap is in the CHD
@ -129,30 +125,30 @@ namespace BizHawk.Emulation.DiscSystem
public CHDParseException(string message, Exception ex) : base(message, ex) { }
}
private static LibChdr.chd_track_type GetTrackType(string type)
private static LibChd.chd_track_type GetTrackType(string type)
{
return type switch
{
"MODE1" => LibChdr.chd_track_type.CD_TRACK_MODE1,
"MODE1/2048" => LibChdr.chd_track_type.CD_TRACK_MODE1,
"MODE1_RAW" => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW,
"MODE1/2352" => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW,
"MODE2" => LibChdr.chd_track_type.CD_TRACK_MODE2,
"MODE2/2336" => LibChdr.chd_track_type.CD_TRACK_MODE2,
"MODE2_FORM1" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1,
"MODE2/2048" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1,
"MODE2_FORM2" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2,
"MODE2/2324" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2,
"MODE2_FORM_MIX" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX,
"MODE2_RAW" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW,
"MODE2/2352" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW,
"CDI/2352" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW,
"AUDIO" => LibChdr.chd_track_type.CD_TRACK_AUDIO,
"MODE1" => LibChd.chd_track_type.CD_TRACK_MODE1,
"MODE1/2048" => LibChd.chd_track_type.CD_TRACK_MODE1,
"MODE1_RAW" => LibChd.chd_track_type.CD_TRACK_MODE1_RAW,
"MODE1/2352" => LibChd.chd_track_type.CD_TRACK_MODE1_RAW,
"MODE2" => LibChd.chd_track_type.CD_TRACK_MODE2,
"MODE2/2336" => LibChd.chd_track_type.CD_TRACK_MODE2,
"MODE2_FORM1" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM1,
"MODE2/2048" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM1,
"MODE2_FORM2" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM2,
"MODE2/2324" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM2,
"MODE2_FORM_MIX" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX,
"MODE2_RAW" => LibChd.chd_track_type.CD_TRACK_MODE2_RAW,
"MODE2/2352" => LibChd.chd_track_type.CD_TRACK_MODE2_RAW,
"CDI/2352" => LibChd.chd_track_type.CD_TRACK_MODE2_RAW,
"AUDIO" => LibChd.chd_track_type.CD_TRACK_AUDIO,
_ => throw new CHDParseException("Malformed CHD format: Invalid track type!"),
};
}
private static (LibChdr.chd_track_type TrackType, bool ChdContainsPregap) GetTrackTypeForPregap(string type)
private static (LibChd.chd_track_type TrackType, bool ChdContainsPregap) GetTrackTypeForPregap(string type)
{
if (type.Length > 0 && type[0] == 'V')
{
@ -162,40 +158,40 @@ namespace BizHawk.Emulation.DiscSystem
return (GetTrackType(type), false);
}
private static uint GetSectorSize(LibChdr.chd_track_type type)
private static uint GetSectorSize(LibChd.chd_track_type type)
{
return type switch
{
LibChdr.chd_track_type.CD_TRACK_MODE1 => 2048,
LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => 2352,
LibChdr.chd_track_type.CD_TRACK_MODE2 => 2336,
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => 2048,
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => 2324,
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => 2336,
LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => 2352,
LibChdr.chd_track_type.CD_TRACK_AUDIO => 2352,
LibChd.chd_track_type.CD_TRACK_MODE1 => 2048,
LibChd.chd_track_type.CD_TRACK_MODE1_RAW => 2352,
LibChd.chd_track_type.CD_TRACK_MODE2 => 2336,
LibChd.chd_track_type.CD_TRACK_MODE2_FORM1 => 2048,
LibChd.chd_track_type.CD_TRACK_MODE2_FORM2 => 2324,
LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX => 2336,
LibChd.chd_track_type.CD_TRACK_MODE2_RAW => 2352,
LibChd.chd_track_type.CD_TRACK_AUDIO => 2352,
_ => throw new CHDParseException("Malformed CHD format: Invalid track type!"),
};
}
private static LibChdr.chd_sub_type GetSubType(string type)
private static LibChd.chd_sub_type GetSubType(string type)
{
return type switch
{
"RW" => LibChdr.chd_sub_type.CD_SUB_NORMAL,
"RW_RAW" => LibChdr.chd_sub_type.CD_SUB_RAW,
"NONE" => LibChdr.chd_sub_type.CD_SUB_NONE,
"RW" => LibChd.chd_sub_type.CD_SUB_NORMAL,
"RW_RAW" => LibChd.chd_sub_type.CD_SUB_RAW,
"NONE" => LibChd.chd_sub_type.CD_SUB_NONE,
_ => throw new CHDParseException("Malformed CHD format: Invalid sub type!"),
};
}
private static uint GetSubSize(LibChdr.chd_sub_type type)
private static uint GetSubSize(LibChd.chd_sub_type type)
{
return type switch
{
LibChdr.chd_sub_type.CD_SUB_NORMAL => 96,
LibChdr.chd_sub_type.CD_SUB_RAW => 96,
LibChdr.chd_sub_type.CD_SUB_NONE => 0,
LibChd.chd_sub_type.CD_SUB_NORMAL => 96,
LibChd.chd_sub_type.CD_SUB_RAW => 96,
LibChd.chd_sub_type.CD_SUB_NONE => 0,
_ => throw new CHDParseException("Malformed CHD format: Invalid sub type!"),
};
}
@ -312,8 +308,8 @@ namespace BizHawk.Emulation.DiscSystem
};
if (bigEndian)
{
cdMetadata.TrackType = (LibChdr.chd_track_type)BinaryPrimitives.ReadUInt32BigEndian(track);
cdMetadata.SubType = (LibChdr.chd_sub_type)BinaryPrimitives.ReadUInt32BigEndian(track[..4]);
cdMetadata.TrackType = (LibChd.chd_track_type)BinaryPrimitives.ReadUInt32BigEndian(track);
cdMetadata.SubType = (LibChd.chd_sub_type)BinaryPrimitives.ReadUInt32BigEndian(track[..4]);
cdMetadata.SectorSize = BinaryPrimitives.ReadUInt32BigEndian(track[..8]);
cdMetadata.SubSize = BinaryPrimitives.ReadUInt32BigEndian(track[..12]);
cdMetadata.Frames = BinaryPrimitives.ReadUInt32BigEndian(track[..16]);
@ -321,8 +317,8 @@ namespace BizHawk.Emulation.DiscSystem
}
else
{
cdMetadata.TrackType = (LibChdr.chd_track_type)BinaryPrimitives.ReadUInt32LittleEndian(track);
cdMetadata.SubType = (LibChdr.chd_sub_type)BinaryPrimitives.ReadUInt32LittleEndian(track[..4]);
cdMetadata.TrackType = (LibChd.chd_track_type)BinaryPrimitives.ReadUInt32LittleEndian(track);
cdMetadata.SubType = (LibChd.chd_sub_type)BinaryPrimitives.ReadUInt32LittleEndian(track[..4]);
cdMetadata.SectorSize = BinaryPrimitives.ReadUInt32LittleEndian(track[..8]);
cdMetadata.SubSize = BinaryPrimitives.ReadUInt32LittleEndian(track[..12]);
cdMetadata.Frames = BinaryPrimitives.ReadUInt32LittleEndian(track[..16]);
@ -350,32 +346,52 @@ namespace BizHawk.Emulation.DiscSystem
}
/// <exception cref="CHDParseException">malformed chd format</exception>
public static CHDFile ParseFrom(Stream stream)
public static CHDFile ParseFrom(string path)
{
var chdf = new CHDFile();
try
{
chdf.CoreFile = new(stream);
var err = LibChdr.chd_open_core_file(chdf.CoreFile.CoreFile, LibChdr.CHD_OPEN_READ, IntPtr.Zero, out chdf.ChdFile);
if (err != LibChdr.chd_error.CHDERR_NONE)
// .NET Standard 2.0 doesn't have UnmanagedType.LPUTF8Str :(
// (although .NET Framework has it just fine along with modern .NET)
var nb = Encoding.UTF8.GetMaxByteCount(path.Length);
var ptr = Marshal.AllocCoTaskMem(checked(nb + 1));
try
{
throw new CHDParseException($"Malformed CHD format: Failed to open chd, got error {err}");
unsafe
{
fixed (char* c = path)
{
var pbMem = (byte*)ptr;
var nbWritten = Encoding.UTF8.GetBytes(c, path.Length, pbMem!, nb);
pbMem[nbWritten] = 0;
}
}
var err = LibChd.chd_open(ptr, LibChd.CHD_OPEN_READ, IntPtr.Zero, out chdf.ChdFile);
if (err != LibChd.chd_error.CHDERR_NONE)
{
throw new CHDParseException($"Malformed CHD format: Failed to open chd, got error {err}");
}
err = LibChd.chd_read_header(ptr, ref chdf.Header);
if (err != LibChd.chd_error.CHDERR_NONE)
{
throw new CHDParseException($"Malformed CHD format: Failed to read chd header, got error {err}");
}
}
finally
{
Marshal.FreeCoTaskMem(ptr);
}
unsafe
{
var header = (LibChdr.chd_header*)LibChdr.chd_get_header(chdf.ChdFile);
chdf.Header = *header;
}
if (chdf.Header.hunkbytes == 0 || chdf.Header.hunkbytes % LibChdr.CD_FRAME_SIZE != 0)
if (chdf.Header.hunkbytes == 0 || chdf.Header.hunkbytes % LibChd.CD_FRAME_SIZE != 0)
{
throw new CHDParseException("Malformed CHD format: Invalid hunk size");
}
// libchdr puts the correct value here for older versions of chds which don't have this
// chd-rs puts the correct value here for older versions of chds which don't have this
// for newer chds, it is left as is, which might be invalid
if (chdf.Header.unitbytes != LibChdr.CD_FRAME_SIZE)
if (chdf.Header.unitbytes != LibChd.CD_FRAME_SIZE)
{
throw new CHDParseException("Malformed CHD format: Invalid unit size");
}
@ -383,18 +399,18 @@ namespace BizHawk.Emulation.DiscSystem
var metadataOutput = new byte[256];
for (uint i = 0; i < 99; i++)
{
err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_TRACK_METADATA2_TAG,
var err = LibChd.chd_get_metadata(chdf.ChdFile, LibChd.CDROM_TRACK_METADATA2_TAG,
i, metadataOutput, (uint)metadataOutput.Length, out var resultLen, out _, out _);
if (err == LibChdr.chd_error.CHDERR_NONE)
if (err == LibChd.chd_error.CHDERR_NONE)
{
var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen).TrimEnd('\0');
chdf.CdMetadatas.Add(ParseMetadata2(metadata));
continue;
}
err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_TRACK_METADATA_TAG,
err = LibChd.chd_get_metadata(chdf.ChdFile, LibChd.CDROM_TRACK_METADATA_TAG,
i, metadataOutput, (uint)metadataOutput.Length, out resultLen, out _, out _);
if (err == LibChdr.chd_error.CHDERR_NONE)
if (err == LibChd.chd_error.CHDERR_NONE)
{
var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen).TrimEnd('\0');
chdf.CdMetadatas.Add(ParseMetadata(metadata));
@ -415,9 +431,9 @@ namespace BizHawk.Emulation.DiscSystem
{
// if no metadata was present, we might have "old" metadata instead (which has all track info stored in one entry)
metadataOutput = new byte[4 + 24 * 99];
err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_OLD_METADATA_TAG,
var err = LibChd.chd_get_metadata(chdf.ChdFile, LibChd.CDROM_OLD_METADATA_TAG,
0, metadataOutput, (uint)metadataOutput.Length, out var resultLen, out _, out _);
if (err == LibChdr.chd_error.CHDERR_NONE)
if (err == LibChd.chd_error.CHDERR_NONE)
{
if (resultLen != metadataOutput.Length)
{
@ -447,7 +463,7 @@ namespace BizHawk.Emulation.DiscSystem
}
// pad expected sectors up to the next hunk
var sectorsPerHunk = chdf.Header.hunkbytes / LibChdr.CD_FRAME_SIZE;
var sectorsPerHunk = chdf.Header.hunkbytes / LibChd.CD_FRAME_SIZE;
chdExpectedNumSectors = (chdExpectedNumSectors + sectorsPerHunk - 1) / sectorsPerHunk * sectorsPerHunk;
var chdActualNumSectors = chdf.Header.hunkcount * sectorsPerHunk;
@ -462,15 +478,9 @@ namespace BizHawk.Emulation.DiscSystem
{
if (chdf.ChdFile != IntPtr.Zero)
{
LibChdr.chd_close(chdf.ChdFile);
LibChd.chd_close(chdf.ChdFile);
}
if (chdf.CoreFile == null)
{
stream.Dispose();
}
chdf.CoreFile?.Dispose();
throw ex as CHDParseException ?? new("Malformed CHD format: An unknown exception was thrown while parsing", ex);
}
}
@ -493,8 +503,7 @@ namespace BizHawk.Emulation.DiscSystem
{
if (!File.Exists(path)) throw new CHDParseException("Malformed CHD format: Nonexistent CHD file!");
var infCHD = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
ret.ParsedCHDFile = ParseFrom(infCHD);
ret.ParsedCHDFile = ParseFrom(path);
ret.Valid = true;
}
catch (CHDParseException ex)
@ -571,7 +580,7 @@ namespace BizHawk.Emulation.DiscSystem
try
{
var chdf = loadResults.ParsedCHDFile;
IBlob chdBlob = new Blob_CHD(chdf.CoreFile, chdf.ChdFile, chdf.Header.hunkbytes);
IBlob chdBlob = new Blob_CHD(chdf.ChdFile, chdf.Header.hunkbytes);
disc.DisposableResources.Add(chdBlob);
// chds only support 1 session
@ -584,7 +593,7 @@ namespace BizHawk.Emulation.DiscSystem
var q = default(SubchannelQ);
//absent some kind of policy for how to set it, this is a safe assumption
const byte kADR = 1;
var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO
var control = cdMetadata.TrackType != LibChd.chd_track_type.CD_TRACK_AUDIO
? EControlQ.DATA
: EControlQ.None;
q.SetStatus(kADR, control);
@ -597,36 +606,36 @@ namespace BizHawk.Emulation.DiscSystem
return new() { QData = q };
}
static SS_Base CreateSynth(LibChdr.chd_track_type trackType)
static SS_Base CreateSynth(LibChd.chd_track_type trackType)
{
return trackType switch
{
LibChdr.chd_track_type.CD_TRACK_MODE1 => new SS_Mode1_2048(),
LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => new SS_2352(),
LibChdr.chd_track_type.CD_TRACK_MODE2 => new SS_Mode2_2336(),
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => new SS_Mode2_Form1_2048(),
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => new SS_Mode2_Form2_2324(),
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => new SS_Mode2_2336(),
LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => new SS_2352(),
LibChdr.chd_track_type.CD_TRACK_AUDIO => new SS_CHD_Audio(),
LibChd.chd_track_type.CD_TRACK_MODE1 => new SS_Mode1_2048(),
LibChd.chd_track_type.CD_TRACK_MODE1_RAW => new SS_2352(),
LibChd.chd_track_type.CD_TRACK_MODE2 => new SS_Mode2_2336(),
LibChd.chd_track_type.CD_TRACK_MODE2_FORM1 => new SS_Mode2_Form1_2048(),
LibChd.chd_track_type.CD_TRACK_MODE2_FORM2 => new SS_Mode2_Form2_2324(),
LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX => new SS_Mode2_2336(),
LibChd.chd_track_type.CD_TRACK_MODE2_RAW => new SS_2352(),
LibChd.chd_track_type.CD_TRACK_AUDIO => new SS_CHD_Audio(),
_ => throw new InvalidOperationException(),
};
}
static CueTrackType ToCueTrackType(LibChdr.chd_track_type chdTrackType, bool isCdi)
static CueTrackType ToCueTrackType(LibChd.chd_track_type chdTrackType, bool isCdi)
{
// rough matches, not too important if these are somewhat wrong (they're just used for generated gaps)
return chdTrackType switch
{
LibChdr.chd_track_type.CD_TRACK_MODE1 => CueTrackType.Mode1_2048,
LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => CueTrackType.Mode1_2352,
LibChdr.chd_track_type.CD_TRACK_MODE2 => CueTrackType.Mode2_2336,
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => CueTrackType.Mode2_2336,
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => CueTrackType.Mode2_2336,
LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => CueTrackType.Mode2_2336,
LibChdr.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => CueTrackType.CDI_2352,
LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => CueTrackType.Mode2_2352,
LibChdr.chd_track_type.CD_TRACK_AUDIO => CueTrackType.Audio,
LibChd.chd_track_type.CD_TRACK_MODE1 => CueTrackType.Mode1_2048,
LibChd.chd_track_type.CD_TRACK_MODE1_RAW => CueTrackType.Mode1_2352,
LibChd.chd_track_type.CD_TRACK_MODE2 => CueTrackType.Mode2_2336,
LibChd.chd_track_type.CD_TRACK_MODE2_FORM1 => CueTrackType.Mode2_2336,
LibChd.chd_track_type.CD_TRACK_MODE2_FORM2 => CueTrackType.Mode2_2336,
LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX => CueTrackType.Mode2_2336,
LibChd.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => CueTrackType.CDI_2352,
LibChd.chd_track_type.CD_TRACK_MODE2_RAW => CueTrackType.Mode2_2352,
LibChd.chd_track_type.CD_TRACK_AUDIO => CueTrackType.Audio,
_ => throw new InvalidOperationException(),
};
}
@ -657,7 +666,7 @@ namespace BizHawk.Emulation.DiscSystem
synth.Policy = IN_DiscMountPolicy;
const byte kADR = 1;
var control = cdMetadata.PregapTrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO
var control = cdMetadata.PregapTrackType != LibChd.chd_track_type.CD_TRACK_AUDIO
? EControlQ.DATA
: EControlQ.None;
synth.sq.SetStatus(kADR, control);
@ -676,14 +685,14 @@ namespace BizHawk.Emulation.DiscSystem
// wrap the base synth with our special synth if we have subcode in the chd
ISectorSynthJob2448 chdSynth = cdMetadata.PregapSubType switch
{
LibChdr.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true),
LibChdr.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false),
LibChdr.chd_sub_type.CD_SUB_NONE => synth,
LibChd.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true),
LibChd.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false),
LibChd.chd_sub_type.CD_SUB_NONE => synth,
_ => throw new InvalidOperationException(),
};
disc._Sectors.Add(chdSynth);
chdOffset += LibChdr.CD_FRAME_SIZE;
chdOffset += LibChd.CD_FRAME_SIZE;
}
else
{
@ -708,7 +717,7 @@ namespace BizHawk.Emulation.DiscSystem
synth.BlobOffset = chdOffset;
synth.Policy = IN_DiscMountPolicy;
const byte kADR = 1;
var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO
var control = cdMetadata.TrackType != LibChd.chd_track_type.CD_TRACK_AUDIO
? EControlQ.DATA
: EControlQ.None;
synth.sq.SetStatus(kADR, control);
@ -721,17 +730,17 @@ namespace BizHawk.Emulation.DiscSystem
synth.Pause = false;
ISectorSynthJob2448 chdSynth = cdMetadata.SubType switch
{
LibChdr.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true),
LibChdr.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false),
LibChdr.chd_sub_type.CD_SUB_NONE => synth,
LibChd.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true),
LibChd.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false),
LibChd.chd_sub_type.CD_SUB_NONE => synth,
_ => throw new InvalidOperationException(),
};
disc._Sectors.Add(chdSynth);
chdOffset += LibChdr.CD_FRAME_SIZE;
chdOffset += LibChd.CD_FRAME_SIZE;
relMSF++;
}
chdOffset += cdMetadata.Padding * LibChdr.CD_FRAME_SIZE;
chdOffset += cdMetadata.Padding * LibChd.CD_FRAME_SIZE;
for (var i = 0; i < cdMetadata.PostGap; i++)
{
@ -741,7 +750,7 @@ namespace BizHawk.Emulation.DiscSystem
Policy = IN_DiscMountPolicy
};
const byte kADR = 1;
var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO
var control = cdMetadata.TrackType != LibChd.chd_track_type.CD_TRACK_AUDIO
? EControlQ.DATA
: EControlQ.None;
synth.sq.SetStatus(kADR, control);
@ -766,11 +775,11 @@ namespace BizHawk.Emulation.DiscSystem
return SessionFormat.Type10_CDI;
}
if (cdMetadata.TrackType is LibChdr.chd_track_type.CD_TRACK_MODE2
or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1
or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2
or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX
or LibChdr.chd_track_type.CD_TRACK_MODE2_RAW)
if (cdMetadata.TrackType is LibChd.chd_track_type.CD_TRACK_MODE2
or LibChd.chd_track_type.CD_TRACK_MODE2_FORM1
or LibChd.chd_track_type.CD_TRACK_MODE2_FORM2
or LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX
or LibChd.chd_track_type.CD_TRACK_MODE2_RAW)
{
return SessionFormat.Type20_CDXA;
}
@ -874,20 +883,20 @@ namespace BizHawk.Emulation.DiscSystem
// write header
// note CHD header has values in big endian, while BinaryWriter will write in little endian
bw.Write(_chdTag);
bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_V5_HEADER_SIZE));
bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_HEADER_VERSION));
bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CHD_V5_HEADER_SIZE));
bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CHD_HEADER_VERSION));
// v5 chd allows for 4 different compression types
// we only have 1 implemented here
bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_CODEC_ZSTD));
bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CHD_CODEC_ZSTD));
bw.Write(0);
bw.Write(0);
bw.Write(0);
bw.Write(0L); // total size of all uncompressed data (written later)
bw.Write(0L); // offset to hunk map (written later)
bw.Write(0L); // offset to first metadata (written later)
bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK)); // bytes per hunk
bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CD_FRAME_SIZE)); // bytes per sector (always CD_FRAME_SIZE)
var blankSha1 = new byte[LibChdr.CHD_SHA1_BYTES];
bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK)); // bytes per hunk
bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CD_FRAME_SIZE)); // bytes per sector (always CD_FRAME_SIZE)
var blankSha1 = new byte[LibChd.CHD_SHA1_BYTES];
bw.Write(blankSha1); // SHA1 of raw data (written later)
bw.Write(blankSha1); // SHA1 of raw data + metadata (written later)
bw.Write(blankSha1); // SHA1 of raw data + metadata for parent (N/A, always 0 for us)
@ -909,12 +918,12 @@ namespace BizHawk.Emulation.DiscSystem
IsCDI = track.Mode == 2 && session.TOC.SessionFormat == SessionFormat.Type10_CDI,
TrackType = track.Mode switch
{
0 => LibChdr.chd_track_type.CD_TRACK_AUDIO,
1 => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW,
2 => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW,
0 => LibChd.chd_track_type.CD_TRACK_AUDIO,
1 => LibChd.chd_track_type.CD_TRACK_MODE1_RAW,
2 => LibChd.chd_track_type.CD_TRACK_MODE2_RAW,
_ => throw new InvalidOperationException(),
},
SubType = LibChdr.chd_sub_type.CD_SUB_RAW,
SubType = LibChd.chd_sub_type.CD_SUB_RAW,
SectorSize = 2352,
SubSize = 96,
Frames = (uint)(track.NextTrack.LBA - firstIndexLba),
@ -934,12 +943,12 @@ namespace BizHawk.Emulation.DiscSystem
using var sha1Inc = IncrementalHash.CreateHash(HashAlgorithmName.SHA1);
using var zstd = new Zstd();
var dsr = new DiscSectorReader(disc) { Policy = { DeinterleavedSubcode = true, DeterministicClearBuffer = true } };
var sectorBuf = new byte[LibChdr.CD_FRAME_SIZE];
var sectorBuf = new byte[LibChd.CD_FRAME_SIZE];
var cdLba = 0;
uint chdLba = 0, chdPos;
var curHunk = new byte[LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK];
var curHunk = new byte[LibChd.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK];
#if false // TODO: cdzs
const uint COMPRESSION_LEN_BYTES = LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK < 65536 ? 2 : 3;
const uint COMPRESSION_LEN_BYTES = LibChd.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK < 65536 ? 2 : 3;
const uint ECC_BYTES = (CD_FRAMES_PER_HUNK + 7) / 8;
var hunkHeader = new byte[COMPRESSION_LEN_BYTES + ECC_BYTES];
#endif
@ -949,9 +958,6 @@ namespace BizHawk.Emulation.DiscSystem
var hunkOffset = bw.BaseStream.Position;
// TODO: adjust compression level?
// note: it's fairly important a high compression level is chosen
// libchdr will assume a compressed hunk with not be larger than an uncompressed hunk
// but with low compression levels, this is not necessarily true
using (var cstream = zstd.CreateZstdCompressionStream(bw.BaseStream, Zstd.MaxCompressionLevel))
{
cstream.Write(curHunk, 0, curHunk.Length);
@ -976,7 +982,7 @@ namespace BizHawk.Emulation.DiscSystem
// audio samples are byteswapped, so make sure to account for that
var trackType = i < cdMetadata.Pregap ? cdMetadata.PregapTrackType : cdMetadata.TrackType;
if (trackType == LibChdr.chd_track_type.CD_TRACK_AUDIO)
if (trackType == LibChd.chd_track_type.CD_TRACK_AUDIO)
{
EndiannessUtils.MutatingByteSwap16(sectorBuf.AsSpan()[..2352]);
}
@ -986,11 +992,11 @@ namespace BizHawk.Emulation.DiscSystem
Buffer.BlockCopy(sectorBuf, 0, curHunk, (int)(2352U * chdPos), 2352);
Buffer.BlockCopy(sectorBuf, 2352, curHunk, (int)(2352U * CD_FRAMES_PER_HUNK + 96U * chdPos), 96);
#else
Buffer.BlockCopy(sectorBuf, 0, curHunk, (int)(LibChdr.CD_FRAME_SIZE * chdPos), (int)LibChdr.CD_FRAME_SIZE);
Buffer.BlockCopy(sectorBuf, 0, curHunk, (int)(LibChd.CD_FRAME_SIZE * chdPos), (int)LibChd.CD_FRAME_SIZE);
#endif
if (chdPos == CD_FRAMES_PER_HUNK - 1)
{
EndHunk(CD_FRAMES_PER_HUNK * LibChdr.CD_FRAME_SIZE);
EndHunk(CD_FRAMES_PER_HUNK * LibChd.CD_FRAME_SIZE);
}
cdLba++;
@ -1002,7 +1008,7 @@ namespace BizHawk.Emulation.DiscSystem
chdPos = chdLba % CD_FRAMES_PER_HUNK;
if (chdPos == CD_FRAMES_PER_HUNK - 1)
{
EndHunk(CD_FRAMES_PER_HUNK * LibChdr.CD_FRAME_SIZE);
EndHunk(CD_FRAMES_PER_HUNK * LibChd.CD_FRAME_SIZE);
}
chdLba++;
@ -1013,18 +1019,18 @@ namespace BizHawk.Emulation.DiscSystem
chdPos = chdLba % CD_FRAMES_PER_HUNK;
if (chdPos != 0)
{
EndHunk(chdPos * LibChdr.CD_FRAME_SIZE);
EndHunk(chdPos * LibChd.CD_FRAME_SIZE);
}
static string TrackTypeStr(LibChdr.chd_track_type trackType, bool isCdi)
static string TrackTypeStr(LibChd.chd_track_type trackType, bool isCdi)
{
// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
return trackType switch
{
LibChdr.chd_track_type.CD_TRACK_AUDIO => "AUDIO",
LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => "MODE1_RAW",
LibChdr.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => "CDI/2352",
LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => "MODE2_RAW",
LibChd.chd_track_type.CD_TRACK_AUDIO => "AUDIO",
LibChd.chd_track_type.CD_TRACK_MODE1_RAW => "MODE1_RAW",
LibChd.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => "CDI/2352",
LibChd.chd_track_type.CD_TRACK_MODE2_RAW => "MODE2_RAW",
_ => throw new InvalidOperationException(),
};
}
@ -1045,8 +1051,8 @@ namespace BizHawk.Emulation.DiscSystem
var metadataStr = $"TRACK:{cdMetadata.Track} TYPE:{trackType} SUBTYPE:RW_RAW FRAMES:{cdMetadata.Frames} PREGAP:{cdMetadata.Pregap} PGTYPE:{pgTrackType} PGSUB:RW_RAW POSTGAP:0\0";
var metadataBytes = Encoding.ASCII.GetBytes(metadataStr);
bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CDROM_TRACK_METADATA2_TAG));
bw.Write(LibChdr.CHD_MDFLAGS_CHECKSUM);
bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CDROM_TRACK_METADATA2_TAG));
bw.Write(LibChd.CHD_MDFLAGS_CHECKSUM);
var chunkDataSize = new byte[3]; // 24 bit integer
chunkDataSize[0] = (byte)((metadataBytes.Length >> 16) & 0xFF);
chunkDataSize[1] = (byte)((metadataBytes.Length >> 8) & 0xFF);
@ -1162,7 +1168,7 @@ namespace BizHawk.Emulation.DiscSystem
bw.Write(BinaryPrimitives.ReverseEndianness((uint)(hunkMapEnd - hunkMapOffset - 16)));
bw.BaseStream.Seek(0x20, SeekOrigin.Begin);
bw.Write(BinaryPrimitives.ReverseEndianness(chdLba * (long)LibChdr.CD_FRAME_SIZE));
bw.Write(BinaryPrimitives.ReverseEndianness(chdLba * (long)LibChd.CD_FRAME_SIZE));
bw.Write(BinaryPrimitives.ReverseEndianness(hunkMapOffset));
bw.Write(BinaryPrimitives.ReverseEndianness(metadataOffset));
@ -1191,10 +1197,10 @@ namespace BizHawk.Emulation.DiscSystem
// tag is hashed alongside the hash
// we use the same tag every time, so we can just reuse this array
var metadataTag = new byte[4];
metadataTag[0] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 24) & 0xFF);
metadataTag[1] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 16) & 0xFF);
metadataTag[2] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 8) & 0xFF);
metadataTag[3] = (byte)(LibChdr.CDROM_TRACK_METADATA2_TAG & 0xFF);
metadataTag[0] = (byte)((LibChd.CDROM_TRACK_METADATA2_TAG >> 24) & 0xFF);
metadataTag[1] = (byte)((LibChd.CDROM_TRACK_METADATA2_TAG >> 16) & 0xFF);
metadataTag[2] = (byte)((LibChd.CDROM_TRACK_METADATA2_TAG >> 8) & 0xFF);
metadataTag[3] = (byte)(LibChd.CDROM_TRACK_METADATA2_TAG & 0xFF);
foreach (var metadataHash in metadataHashes)
{
sha1Inc.AppendData(metadataTag);

View File

@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
#pragma warning disable IDE1006
@ -10,10 +9,11 @@ using System.Runtime.InteropServices;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// libchdr bindings
/// Bindings matching libchdr's chd.h
/// In practice, we use chd-rs, whose c api matches libchdr's
/// TODO: should this be common-ized? chd isn't limited to discs, it could be used for hard disk images (e.g. for MAME)
/// </summary>
public static class LibChdr
public static class LibChd
{
public const uint CHD_HEADER_VERSION = 5;
public const uint CHD_V5_HEADER_SIZE = 124;
@ -69,148 +69,7 @@ namespace BizHawk.Emulation.DiscSystem
CHDERR_UNSUPPORTED_FORMAT
}
[StructLayout(LayoutKind.Sequential)]
public struct chd_core_file
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate ulong FSizeDelegate(IntPtr file);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate nuint FReadDelegate(IntPtr buffer, nuint size, nuint count, IntPtr file);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int FCloseDelegate(IntPtr file);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int FSeekDelegate(IntPtr file, long offset, SeekOrigin origin);
public IntPtr argp;
[MarshalAs(UnmanagedType.FunctionPtr)]
public FSizeDelegate fsize;
[MarshalAs(UnmanagedType.FunctionPtr)]
public FReadDelegate fread;
[MarshalAs(UnmanagedType.FunctionPtr)]
public FCloseDelegate fclose;
[MarshalAs(UnmanagedType.FunctionPtr)]
public FSeekDelegate fseek;
}
/// <summary>
/// Convenience chd_core_file wrapper against a generic Stream
/// </summary>
public class CoreFileStreamWrapper : IDisposable
{
private const uint READ_BUFFER_LEN = 8 * CD_FRAME_SIZE; // 8 frames, usual uncompressed hunk size
private readonly byte[] _readBuffer = new byte[READ_BUFFER_LEN];
private Stream _s;
// ReSharper disable once MemberCanBePrivate.Global
private readonly chd_core_file _coreFile;
public readonly IntPtr CoreFile;
private ulong FSize(IntPtr file)
{
try
{
return (ulong)_s.Length;
}
catch (Exception e)
{
Console.Error.WriteLine(e);
return unchecked((ulong)-1);
}
}
private nuint FRead(IntPtr buffer, nuint size, nuint count, IntPtr file)
{
nuint ret = 0;
try
{
// note: size will always be 1, so this should never overflow
var numBytesToRead = (uint)Math.Min(size * (ulong)count, uint.MaxValue);
while (numBytesToRead > 0)
{
var numRead = _s.Read(_readBuffer, 0, (int)Math.Min(READ_BUFFER_LEN, numBytesToRead));
if (numRead == 0)
{
return ret;
}
Marshal.Copy(_readBuffer, 0, buffer, numRead);
buffer += numRead;
ret += (uint)numRead;
numBytesToRead -= (uint)numRead;
}
return ret;
}
catch (Exception e)
{
Console.Error.WriteLine(e);
return ret;
}
}
private int FClose(IntPtr file)
{
if (_s == null)
{
return -1;
}
_s.Dispose();
_s = null;
return 0;
}
private int FSeek(IntPtr file, long offset, SeekOrigin origin)
{
try
{
_s.Seek(offset, origin);
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return -1;
}
}
public CoreFileStreamWrapper(Stream s)
{
if (!s.CanRead || !s.CanSeek)
{
throw new NotSupportedException("The underlying CHD stream must support reading and seeking!");
}
_s = s;
_coreFile.fsize = FSize;
_coreFile.fread = FRead;
_coreFile.fclose = FClose;
_coreFile.fseek = FSeek;
// the pointer here must stay alloc'd on the unmanaged size
// as libchdr expects the memory to not move around
CoreFile = Marshal.AllocCoTaskMem(Marshal.SizeOf<chd_core_file>());
Marshal.StructureToPtr(_coreFile, CoreFile, fDeleteOld: false);
}
public void Dispose()
{
Marshal.DestroyStructure<chd_core_file>(CoreFile);
Marshal.FreeCoTaskMem(CoreFile);
_s?.Dispose();
}
}
[DllImport("chdr")]
public static extern chd_error chd_open_core_file(IntPtr file, int mode, IntPtr parent, out IntPtr chd);
[DllImport("chdr")]
public static extern void chd_close(IntPtr chd);
// extracted chd header (not the same as the one on disk, but rather an interpreted one by libchdr)
// extracted chd header (not the same as the one on disk, but rather an interpreted one by chd-rs)
[StructLayout(LayoutKind.Sequential)]
public struct chd_header
{
@ -239,12 +98,18 @@ namespace BizHawk.Emulation.DiscSystem
public uint obsolete_hunksize; // obsolete field -- do not use!
}
[DllImport("chdr")]
public static extern IntPtr chd_get_header(IntPtr chd);
[DllImport("chd_capi")]
public static extern chd_error chd_open(IntPtr filename, int mode, IntPtr parent, out IntPtr chd);
[DllImport("chdr")]
[DllImport("chd_capi")]
public static extern void chd_close(IntPtr chd);
[DllImport("chd_capi")]
public static extern chd_error chd_read(IntPtr chd, uint hunknum, byte[] buffer);
[DllImport("chd_capi")]
public static extern chd_error chd_read_header(IntPtr filename, ref chd_header header);
public enum chd_track_type : uint
{
CD_TRACK_MODE1 = 0, // mode 1 2048 bytes/sector
@ -267,7 +132,7 @@ namespace BizHawk.Emulation.DiscSystem
// hunks should be a multiple of this for cd chds
public const uint CD_FRAME_SIZE = 2352 + 96;
[DllImport("chdr")]
[DllImport("chd_capi")]
public static extern chd_error chd_get_metadata(
IntPtr chd, uint searchtag, uint searchindex, byte[] output, uint outputlen, out uint resultlen, out uint resulttag, out byte resultflags);
}