use super::common::DEFAULT_RAW_CODECS;
use crate::error::DiscError;
use chdlady_core::{ChdFile, ChdWriteConfig, ChdWriter, CreateResult, ProgressStatus};
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct CreateRawOptions {
pub start_byte: Option<u64>,
pub start_hunk: Option<u64>,
pub length_bytes: Option<u64>,
pub length_hunks: Option<u64>,
pub hunk_size: Option<u32>,
pub unit_size: u32,
pub codecs: Option<[u32; 4]>,
pub parent_path: Option<PathBuf>,
pub num_processors: Option<usize>,
pub cancel_token: Option<Arc<AtomicBool>>,
}
pub fn create_raw<F>(
input_path: &Path,
output_path: &Path,
options: &CreateRawOptions,
mut progress: F,
) -> Result<CreateResult, DiscError>
where
F: FnMut(ProgressStatus),
{
let mut input_file = File::open(input_path)?;
let total_size = input_file.metadata()?.len();
let unit_size = options.unit_size;
if unit_size == 0 {
return Err(DiscError::InvalidMetadata(
"unit size must be non-zero".into(),
));
}
let hunk_size = options
.hunk_size
.unwrap_or_else(|| ((4096 / unit_size) * unit_size).max(unit_size));
let start = match (options.start_byte, options.start_hunk) {
(Some(b), _) => b,
(None, Some(h)) => h * hunk_size as u64,
(None, None) => 0,
};
let length = match (options.length_bytes, options.length_hunks) {
(Some(b), _) => b,
(None, Some(h)) => h * hunk_size as u64,
(None, None) => total_size.saturating_sub(start),
};
let end = start
.checked_add(length)
.ok_or_else(|| DiscError::InvalidMetadata("specified range overflows u64".into()))?;
if end > total_size {
return Err(DiscError::InvalidMetadata(
"specified range exceeds input file size".into(),
));
}
if !length.is_multiple_of(unit_size as u64) {
return Err(DiscError::InvalidMetadata(format!(
"data length {} is not divisible by unit size {}",
length, unit_size
)));
}
let (parent_map, parent_sha1) = if let Some(ref p) = options.parent_path {
let pf = File::open(p)?;
let mut parent_chd = ChdFile::open(pf)?;
let p_sha1 = parent_chd.header().sha1;
let map = ChdWriter::build_parent_map(&mut parent_chd, hunk_size, unit_size)?;
(Some(map), p_sha1)
} else {
(None, [0u8; 20])
};
let compressors = options.codecs.unwrap_or(DEFAULT_RAW_CODECS);
let config = ChdWriteConfig {
logical_bytes: length,
hunk_bytes: hunk_size,
unit_bytes: unit_size,
compressors,
parent_sha1,
num_processors: options.num_processors,
cancel_token: options.cancel_token.clone(),
};
input_file.seek(SeekFrom::Start(start))?;
let mut reader = BufReader::with_capacity(1024 * 1024, input_file.take(length));
let output_file = File::create(output_path)?;
let mut writer = BufWriter::new(output_file);
let result = ChdWriter::write_chd_with_parent(
&mut reader,
&mut writer,
&config,
&[],
parent_map.as_ref(),
&mut progress,
);
match result {
Ok(res) => Ok(res),
Err(e) => {
let _ = std::fs::remove_file(output_path);
Err(DiscError::Chd(e))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_raw_cancellation() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_raw_cancel_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let raw_path = temp_dir.join("test.bin");
let out_path = temp_dir.join("output.chd");
let test_data = vec![0x42u8; 65536];
std::fs::write(&raw_path, &test_data).unwrap();
let cancel_token = Arc::new(AtomicBool::new(true));
let options = CreateRawOptions {
unit_size: 512,
cancel_token: Some(cancel_token),
..Default::default()
};
let result = create_raw(&raw_path, &out_path, &options, |_| {});
match result {
Err(DiscError::Chd(chdlady_core::ChdError::Cancelled)) => {}
other => panic!("expected Err(DiscError::Chd(Cancelled)), got {:?}", other),
}
assert!(
!out_path.exists(),
"incomplete output CHD file should be deleted on cancellation"
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
}