first draft
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "cdrustanoia_cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-2.0-or-later"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "cdrustanoia"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "cdparanoia"
|
||||
path = "src/bin_cdparanoia.rs"
|
||||
|
||||
[lib]
|
||||
name = "cdrustanoia_cli"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
cdrustanoia_core.workspace = true
|
||||
libc.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -0,0 +1,295 @@
|
||||
//! Drive cache/timing analysis (`-A`), based on cachetest.c.
|
||||
//!
|
||||
//! This is a compact but functional port: it measures seek/read timing,
|
||||
//! estimates the random-access cache size, checks cache transfer speed, and
|
||||
//! verifies that a backward seek flushes the cache. Output wording follows
|
||||
//! the original where it matters to users.
|
||||
|
||||
use crate::log::Log;
|
||||
use crate::{log_c, print_c, report_c};
|
||||
use cdrustanoia_core::device::{Drive, CD_FRAMESIZE_RAW};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
const MIN_SEEK_MS: i32 = 6;
|
||||
|
||||
fn read_timed(d: &mut dyn Drive, lba: i64, len: i64) -> (i64, i32) {
|
||||
let t = d.read_timed(None, lba, len);
|
||||
match t.result {
|
||||
Ok(n) => (n as i64, t.milliseconds),
|
||||
Err(e) => {
|
||||
if e.errno == libc::ENOMEDIUM {
|
||||
(-404, -1)
|
||||
} else {
|
||||
(-1, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn time_drive(d: &mut dyn Drive, log: &mut Log, lba: i64, len: i64, initial_seek: bool) -> i64 {
|
||||
log_c!(log, "\n");
|
||||
let mut sofar = 0i64;
|
||||
let mut sum = 0f64;
|
||||
let mut sumsq = 0f64;
|
||||
let mut latency = 0i32;
|
||||
let mut i = 0;
|
||||
while sofar < len {
|
||||
let toread = if i == 0 && initial_seek {
|
||||
1
|
||||
} else {
|
||||
len - sofar
|
||||
};
|
||||
let (ret, x) = read_timed(d, lba + sofar, toread);
|
||||
if ret <= 0 {
|
||||
return if ret == -404 { -404 } else { -1 };
|
||||
}
|
||||
let x = x.clamp(0, 9999);
|
||||
log_c!(log, "{}:{}:{} ", lba + sofar, ret, x);
|
||||
sofar += ret;
|
||||
if i != 0 || !initial_seek {
|
||||
sum += x as f64;
|
||||
sumsq += (x * x) as f64 / ret as f64;
|
||||
} else {
|
||||
latency = x;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let denom = (len - 1).max(1) as f64;
|
||||
let mean = sum / denom;
|
||||
let stddev = ((sumsq / denom - mean * mean).max(0.0)).sqrt();
|
||||
if initial_seek {
|
||||
print_c!(
|
||||
log,
|
||||
"{:4}ms seek, {:.2}ms/sec read [{:.1}x]",
|
||||
latency,
|
||||
mean,
|
||||
1000.0 / 75.0 / mean.max(0.001)
|
||||
);
|
||||
log_c!(
|
||||
log,
|
||||
"\n\tInitial seek latency ({} sectors): {}ms",
|
||||
len,
|
||||
latency
|
||||
);
|
||||
}
|
||||
log_c!(
|
||||
log,
|
||||
"\n\tAverage read latency: {:.2}ms/sector (raw speed: {:.1}x)",
|
||||
mean,
|
||||
1000.0 / 75.0 / mean.max(0.001)
|
||||
);
|
||||
log_c!(
|
||||
log,
|
||||
"\n\tRead latency standard deviation: {:.2}ms/sector",
|
||||
stddev
|
||||
);
|
||||
sum as i64
|
||||
}
|
||||
|
||||
fn retime_drive(d: &mut dyn Drive, log: &mut Log, lba: i64, readahead: i64, oldmean: f64) -> f64 {
|
||||
let mut sectors = 2000i64;
|
||||
if sectors as f64 * oldmean > 5000.0 {
|
||||
sectors = (5000.0 / oldmean.max(0.001)) as i64;
|
||||
}
|
||||
let readahead = readahead * 10 / 9;
|
||||
if readahead > sectors {
|
||||
sectors = readahead;
|
||||
}
|
||||
let total = time_drive(d, log, lba, sectors, true);
|
||||
if total <= 0 {
|
||||
return oldmean;
|
||||
}
|
||||
let newmean = total as f64 / sectors as f64;
|
||||
log_c!(
|
||||
log,
|
||||
"\n\tOld mean={:.2}ms/sec, New mean={:.2}ms/sec\n",
|
||||
oldmean,
|
||||
newmean
|
||||
);
|
||||
newmean.max(oldmean)
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
pub fn analyze_cache(d: &mut dyn Drive, log: &mut Log, speed: i32) -> i32 {
|
||||
let speed = if speed <= 0 { -1 } else { speed };
|
||||
report_c!(
|
||||
log,
|
||||
"\n=================== Checking drive cache/timing behavior ===================\n"
|
||||
);
|
||||
|
||||
// Longest contiguous audio stretch.
|
||||
let mut firstsector = -1i64;
|
||||
let mut lastsector = -1i64;
|
||||
let mut firsttest = -1i64;
|
||||
let mut lasttest = -1i64;
|
||||
for i in 1..=d.tracks() {
|
||||
if d.track_audiop(i) {
|
||||
if firsttest == -1 {
|
||||
firsttest = d.track_firstsector(i);
|
||||
}
|
||||
lasttest = d.track_lastsector(i);
|
||||
if lasttest - firsttest > lastsector - firstsector {
|
||||
firstsector = firsttest;
|
||||
lastsector = lasttest;
|
||||
}
|
||||
} else {
|
||||
firsttest = -1;
|
||||
}
|
||||
}
|
||||
if firstsector == -1 {
|
||||
report_c!(
|
||||
log,
|
||||
"\n\tNo audio on disc; Cannot determine timing behavior..."
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let avail = lastsector - firstsector + 1;
|
||||
let current = (1000i64).min((avail / 2).max(1));
|
||||
let mut offset = firstsector + 10;
|
||||
if offset + current > lastsector {
|
||||
offset = firstsector;
|
||||
}
|
||||
|
||||
report_c!(log, "\nSeek/read timing:\n");
|
||||
let total = time_drive(d, log, offset, current, true);
|
||||
if total == -404 {
|
||||
return -1;
|
||||
}
|
||||
if total > 0 {
|
||||
let mspersector = total as f64 / current as f64;
|
||||
report_c!(
|
||||
log,
|
||||
"\n\tAverage access speed: {:.2}ms/sector [{:.1}x]",
|
||||
mspersector,
|
||||
1000.0 / 75.0 / mspersector.max(0.001)
|
||||
);
|
||||
}
|
||||
|
||||
report_c!(log, "\n\nAnalyzing cache behavior...\n");
|
||||
|
||||
// Find the end of the drive's random-access cache by preloading a
|
||||
// linear run and timing a read of its first sector.
|
||||
let mut cache_size = 0i64;
|
||||
let mut c = 1i64;
|
||||
while c <= 1200 {
|
||||
let (r1, _) = read_timed(d, offset + c, 1);
|
||||
if r1 <= 0 {
|
||||
offset += c + 100;
|
||||
if offset + c > lastsector {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (r2, ms) = read_timed(d, offset, 1);
|
||||
if r2 <= 0 {
|
||||
offset += c + 100;
|
||||
if offset + c > lastsector {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ms >= MIN_SEEK_MS {
|
||||
break;
|
||||
}
|
||||
cache_size = c;
|
||||
c += 64;
|
||||
}
|
||||
|
||||
if cache_size == 0 {
|
||||
report_c!(
|
||||
log,
|
||||
"\tDrive does not cache nonlinear access \n"
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
report_c!(
|
||||
log,
|
||||
"\tApproximate random access cache size: {} sector(s) \n",
|
||||
cache_size
|
||||
);
|
||||
let mut warn = 0;
|
||||
if cache_size > 1200 {
|
||||
report_c!(
|
||||
log,
|
||||
"\nWARNING: This drive appears to be caching more sectors of\n\
|
||||
readahead than Paranoia can currently handle!\n"
|
||||
);
|
||||
warn = 1;
|
||||
}
|
||||
|
||||
if speed == -1 {
|
||||
log_c!(log, "\tAttempting to reset read speed to full... ");
|
||||
} else {
|
||||
log_c!(log, "\tAttempting to reset read speed to {}... ", speed);
|
||||
}
|
||||
if d.speed_set(speed) != 0 {
|
||||
log_c!(log, "failed.\n");
|
||||
} else {
|
||||
log_c!(log, "drive said OK\n");
|
||||
}
|
||||
|
||||
// Cache transfer speed and backseek flushing.
|
||||
let readsize = (cache_size - 8).max(1).min(cache_size);
|
||||
if readsize >= 7 {
|
||||
report_c!(log, "\tTesting cache transfer speed...");
|
||||
let (_, _) = read_timed(d, offset, readsize);
|
||||
let mut elapsed = 0i64;
|
||||
let mut sectors = 0i64;
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed().as_millis() < 1000 {
|
||||
let (n, ms) = read_timed(d, offset, readsize);
|
||||
if n <= 0 {
|
||||
break;
|
||||
}
|
||||
elapsed += ms as i64;
|
||||
sectors += n;
|
||||
}
|
||||
let cachems = if sectors > 0 {
|
||||
elapsed as f64 / sectors as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
report_c!(
|
||||
log,
|
||||
"\t Cache read speed: {:.2}ms/sector [{}x]\n",
|
||||
cachems,
|
||||
(1000.0 / 75.0 / cachems.max(0.001)) as i64
|
||||
);
|
||||
|
||||
report_c!(log, "\tTesting that backseek flushes cache...");
|
||||
let seekpos = (offset + cache_size + 20000).min((lastsector - 150).max(offset + 1));
|
||||
let (r, _) = read_timed(d, seekpos, 1);
|
||||
let mut readms = 0.0;
|
||||
if r > 0 {
|
||||
let total = time_drive(d, log, offset + 1, readsize, true);
|
||||
if total > 0 {
|
||||
readms = total as f64 / readsize as f64;
|
||||
}
|
||||
}
|
||||
report_c!(
|
||||
log,
|
||||
"\t Access speed after backseek: {:.2}ms/sector [{}x]\n",
|
||||
readms,
|
||||
(1000.0 / 75.0 / readms.max(0.001)) as i64
|
||||
);
|
||||
if readms > 0.0 && readms * 2.0 < cachems.max(0.001) {
|
||||
report_c!(
|
||||
log,
|
||||
"\tWARNING: Read timing after backseek faster than expected!\n\
|
||||
\t It's possible/likely that this drive is not\n\
|
||||
\t flushing the readahead cache on backward seeks!\n\n"
|
||||
);
|
||||
warn = 1;
|
||||
} else if readms > 0.0 {
|
||||
report_c!(log, "\tBackseek flushes the cache as expected\n");
|
||||
}
|
||||
let _ = retime_drive(d, log, offset, readsize, cachems.max(1.0));
|
||||
}
|
||||
|
||||
// A short pause mirrors the original's readahead settling delay.
|
||||
sleep(Duration::from_millis(50));
|
||||
let _ = CD_FRAMESIZE_RAW;
|
||||
warn
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
//! GNU getopt-compatible command line parsing for cdparanoia 10.2.
|
||||
|
||||
use crate::output::OutputFormat;
|
||||
use cdrustanoia_core::paranoia::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Args {
|
||||
pub analyze: bool,
|
||||
pub verbose: bool,
|
||||
pub quiet: bool,
|
||||
pub stderr_progress: bool,
|
||||
pub log_summary: Option<Option<String>>,
|
||||
pub log_debug: Option<Option<String>>,
|
||||
pub version: bool,
|
||||
pub query: bool,
|
||||
pub batch: bool,
|
||||
pub search: bool,
|
||||
pub help: bool,
|
||||
pub output_format: OutputFormat,
|
||||
pub force_endian: Option<i32>,
|
||||
pub force_sectors: Option<i32>,
|
||||
pub force_overlap: Option<i32>,
|
||||
pub force_device: Option<String>,
|
||||
pub force_cooked_device: Option<String>,
|
||||
pub force_generic_device: Option<String>,
|
||||
pub force_speed: Option<i32>,
|
||||
pub toc_offset: i32,
|
||||
pub toc_bias: bool,
|
||||
pub sample_offset: i32,
|
||||
pub max_retries: i32,
|
||||
pub paranoia_mode: i32,
|
||||
pub abort_on_skip: bool,
|
||||
pub info_file: Option<String>,
|
||||
pub span: Option<String>,
|
||||
pub outfile: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Args {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
analyze: false,
|
||||
verbose: false,
|
||||
quiet: false,
|
||||
stderr_progress: false,
|
||||
log_summary: None,
|
||||
log_debug: None,
|
||||
version: false,
|
||||
query: false,
|
||||
batch: false,
|
||||
search: false,
|
||||
help: false,
|
||||
output_format: OutputFormat::Wav,
|
||||
force_endian: None,
|
||||
force_sectors: None,
|
||||
force_overlap: None,
|
||||
force_device: None,
|
||||
force_cooked_device: None,
|
||||
force_generic_device: None,
|
||||
force_speed: None,
|
||||
toc_offset: 0,
|
||||
toc_bias: false,
|
||||
sample_offset: 0,
|
||||
max_retries: 20,
|
||||
paranoia_mode: PARANOIA_MODE_FULL ^ PARANOIA_MODE_NEVERSKIP,
|
||||
abort_on_skip: false,
|
||||
info_file: None,
|
||||
span: None,
|
||||
outfile: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn atoi(s: &str) -> i32 {
|
||||
let s = s.trim_start();
|
||||
let mut chars = s.chars().peekable();
|
||||
let mut sign = 1i64;
|
||||
if let Some(c) = chars.peek() {
|
||||
if *c == '-' {
|
||||
sign = -1;
|
||||
chars.next();
|
||||
} else if *c == '+' {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
let mut value = 0i64;
|
||||
let mut any = false;
|
||||
for c in chars {
|
||||
if let Some(d) = c.to_digit(10) {
|
||||
any = true;
|
||||
value = value.saturating_mul(10).saturating_add(d as i64);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return 0;
|
||||
}
|
||||
(sign * value) as i32
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Short {
|
||||
NoArg(char),
|
||||
Required(char),
|
||||
Optional(char),
|
||||
}
|
||||
|
||||
fn short_kind(c: char) -> Option<Short> {
|
||||
match c {
|
||||
'e' | 's' | 'c' | 'C' | 'p' | 'r' | 'R' | 'w' | 'a' | 'f' | 'v' | 'q' | 'V' | 'Q' | 'h'
|
||||
| 'Z' | 'Y' | 'X' | 'W' | 'B' | 'F' | 'T' | 'A' => Some(Short::NoArg(c)),
|
||||
'n' | 'o' | 'O' | 'd' | 'g' | 'k' | 'S' | 'i' | 't' => Some(Short::Required(c)),
|
||||
'z' | 'l' | 'L' => Some(Short::Optional(c)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn long_kind(name: &str) -> Option<(&'static str, Short)> {
|
||||
let table: &[(&str, Short)] = &[
|
||||
("stderr-progress", Short::NoArg('e')),
|
||||
("search-for-drive", Short::NoArg('s')),
|
||||
("force-cdrom-little-endian", Short::NoArg('c')),
|
||||
("force-cdrom-big-endian", Short::NoArg('C')),
|
||||
("force-default-sectors", Short::Required('n')),
|
||||
("force-search-overlap", Short::Required('o')),
|
||||
("force-cdrom-device", Short::Required('d')),
|
||||
("force-cooked-device", Short::Required('k')),
|
||||
("force-generic-device", Short::Required('g')),
|
||||
("force-read-speed", Short::Required('S')),
|
||||
("sample-offset", Short::Required('O')),
|
||||
("toc-offset", Short::Required('t')),
|
||||
("toc-bias", Short::NoArg('T')),
|
||||
("output-raw", Short::NoArg('p')),
|
||||
("output-raw-little-endian", Short::NoArg('r')),
|
||||
("output-raw-big-endian", Short::NoArg('R')),
|
||||
("output-wav", Short::NoArg('w')),
|
||||
("output-aiff", Short::NoArg('f')),
|
||||
("output-aifc", Short::NoArg('a')),
|
||||
("batch", Short::NoArg('B')),
|
||||
("verbose", Short::NoArg('v')),
|
||||
("quiet", Short::NoArg('q')),
|
||||
("version", Short::NoArg('V')),
|
||||
("query", Short::NoArg('Q')),
|
||||
("help", Short::NoArg('h')),
|
||||
("analyze-drive", Short::NoArg('A')),
|
||||
("disable-paranoia", Short::NoArg('Z')),
|
||||
("disable-extra-paranoia", Short::NoArg('Y')),
|
||||
("abort-on-skip", Short::NoArg('X')),
|
||||
("disable-fragmentation", Short::NoArg('F')),
|
||||
("output-info", Short::Required('i')),
|
||||
("never-skip", Short::Optional('z')),
|
||||
("log-summary", Short::Optional('l')),
|
||||
("log-debug", Short::Optional('L')),
|
||||
];
|
||||
if let Some((_, kind)) = table.iter().find(|(n, _)| *n == name) {
|
||||
return Some(("", *kind));
|
||||
}
|
||||
let matches: Vec<&(&str, Short)> = table.iter().filter(|(n, _)| n.starts_with(name)).collect();
|
||||
if matches.len() == 1 {
|
||||
Some(("", matches[0].1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(args: &mut Args, opt: Short, value: Option<String>) -> Result<(), String> {
|
||||
let c = match opt {
|
||||
Short::NoArg(c) | Short::Required(c) | Short::Optional(c) => c,
|
||||
};
|
||||
match c {
|
||||
'A' => {
|
||||
args.analyze = true;
|
||||
args.query = true;
|
||||
args.log_debug = Some(None);
|
||||
args.verbose = true;
|
||||
}
|
||||
'v' => {
|
||||
args.verbose = true;
|
||||
args.quiet = false;
|
||||
}
|
||||
'q' => {
|
||||
args.verbose = false;
|
||||
args.quiet = true;
|
||||
}
|
||||
'e' => {
|
||||
args.stderr_progress = true;
|
||||
eprintln!("Sending all callbacks to stderr for wrapper script");
|
||||
}
|
||||
'l' => {
|
||||
args.log_summary = Some(value);
|
||||
}
|
||||
'L' => {
|
||||
args.log_debug = Some(value);
|
||||
}
|
||||
'V' => args.version = true,
|
||||
'Q' => args.query = true,
|
||||
's' => args.search = true,
|
||||
'h' => args.help = true,
|
||||
'p' => args.output_format = OutputFormat::RawHost,
|
||||
'r' => args.output_format = OutputFormat::RawLittle,
|
||||
'R' => args.output_format = OutputFormat::RawBig,
|
||||
'w' => args.output_format = OutputFormat::Wav,
|
||||
'f' => args.output_format = OutputFormat::Aiff,
|
||||
'a' => args.output_format = OutputFormat::Aifc,
|
||||
'c' => args.force_endian = Some(0),
|
||||
'C' => args.force_endian = Some(1),
|
||||
'n' => args.force_sectors = Some(atoi(value.as_deref().unwrap_or(""))),
|
||||
'o' => args.force_overlap = Some(atoi(value.as_deref().unwrap_or(""))),
|
||||
'd' => args.force_device = value,
|
||||
'g' => {
|
||||
if args.force_cooked_device.is_some() {
|
||||
return Err("-g option incompatable with -k".to_string());
|
||||
}
|
||||
args.force_cooked_device = None;
|
||||
args.force_generic_device = value;
|
||||
}
|
||||
'k' => {
|
||||
if args.force_generic_device.is_some() || args.force_device.is_some() {
|
||||
return Err("-k option incompatable with -d and -g".to_string());
|
||||
}
|
||||
args.force_cooked_device = value;
|
||||
}
|
||||
'S' => args.force_speed = Some(atoi(value.as_deref().unwrap_or(""))),
|
||||
't' => args.toc_offset = atoi(value.as_deref().unwrap_or("")),
|
||||
'T' => args.toc_bias = true,
|
||||
'O' => args.sample_offset = atoi(value.as_deref().unwrap_or("")),
|
||||
'z' => {
|
||||
if let Some(v) = value {
|
||||
args.max_retries = atoi(&v);
|
||||
args.paranoia_mode &= !PARANOIA_MODE_NEVERSKIP;
|
||||
} else {
|
||||
args.paranoia_mode |= PARANOIA_MODE_NEVERSKIP;
|
||||
}
|
||||
}
|
||||
'Z' => args.paranoia_mode = PARANOIA_MODE_DISABLE,
|
||||
'Y' => {
|
||||
args.paranoia_mode |= PARANOIA_MODE_OVERLAP;
|
||||
args.paranoia_mode &= !PARANOIA_MODE_VERIFY;
|
||||
}
|
||||
'X' => args.abort_on_skip = true,
|
||||
'W' => args.paranoia_mode &= !PARANOIA_MODE_REPAIR,
|
||||
'F' => args.paranoia_mode &= !PARANOIA_MODE_FRAGMENT,
|
||||
'B' => args.batch = true,
|
||||
'i' => args.info_file = value,
|
||||
_ => return Err(format!("Unknown option -{}", c)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse(argv: &[String]) -> Result<Args, String> {
|
||||
let mut args = Args::default();
|
||||
let mut positionals = Vec::new();
|
||||
let mut i = 1usize;
|
||||
let mut stop = false;
|
||||
while i < argv.len() {
|
||||
let arg = &argv[i];
|
||||
if stop || arg == "-" || !arg.starts_with('-') {
|
||||
positionals.push(arg.clone());
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if arg == "--" {
|
||||
stop = true;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if let Some(long) = arg.strip_prefix("--") {
|
||||
let (name, inline) = match long.split_once('=') {
|
||||
Some((n, v)) => (n, Some(v.to_string())),
|
||||
None => (long, None),
|
||||
};
|
||||
let (_, kind) =
|
||||
long_kind(name).ok_or_else(|| format!("unrecognized option '--{}'", name))?;
|
||||
match kind {
|
||||
Short::NoArg(c) => {
|
||||
if inline.is_some() {
|
||||
return Err(format!("option '--{}' doesn't allow an argument", name));
|
||||
}
|
||||
apply(&mut args, Short::NoArg(c), None)?;
|
||||
if args.help || args.version {
|
||||
return Ok(args);
|
||||
}
|
||||
}
|
||||
Short::Required(c) => {
|
||||
let value = match inline {
|
||||
Some(v) => Some(v),
|
||||
None => {
|
||||
i += 1;
|
||||
if i >= argv.len() {
|
||||
return Err(format!("option '--{}' requires an argument", name));
|
||||
}
|
||||
Some(argv[i].clone())
|
||||
}
|
||||
};
|
||||
apply(&mut args, Short::Required(c), value)?;
|
||||
}
|
||||
Short::Optional(c) => apply(&mut args, Short::Optional(c), inline)?,
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let chars: Vec<char> = arg[1..].chars().collect();
|
||||
let mut j = 0usize;
|
||||
while j < chars.len() {
|
||||
let c = chars[j];
|
||||
let kind = short_kind(c).ok_or_else(|| format!("invalid option -- '{}'", c))?;
|
||||
match kind {
|
||||
Short::NoArg(c) => {
|
||||
apply(&mut args, Short::NoArg(c), None)?;
|
||||
if args.help || args.version {
|
||||
return Ok(args);
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
Short::Required(c) => {
|
||||
let rest: String = chars[j + 1..].iter().collect();
|
||||
let value = if !rest.is_empty() {
|
||||
rest
|
||||
} else {
|
||||
i += 1;
|
||||
if i >= argv.len() {
|
||||
return Err(format!("option requires an argument -- '{}'", c));
|
||||
}
|
||||
argv[i].clone()
|
||||
};
|
||||
apply(&mut args, Short::Required(c), Some(value))?;
|
||||
break;
|
||||
}
|
||||
Short::Optional(c) => {
|
||||
let rest: String = chars[j + 1..].iter().collect();
|
||||
apply(
|
||||
&mut args,
|
||||
Short::Optional(c),
|
||||
if rest.is_empty() { None } else { Some(rest) },
|
||||
)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if !positionals.is_empty() {
|
||||
args.span = Some(positionals[0].clone());
|
||||
}
|
||||
if positionals.len() > 1 {
|
||||
args.outfile = Some(positionals[1].clone());
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::output::OutputFormat;
|
||||
|
||||
fn p(args: &[&str]) -> Result<Args, String> {
|
||||
let mut v = vec!["cdparanoia".to_string()];
|
||||
v.extend(args.iter().map(|s| s.to_string()));
|
||||
parse(&v)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clustered_flags() {
|
||||
let a = p(&["-vQ"]).unwrap();
|
||||
assert!(a.verbose && !a.quiet && a.query);
|
||||
let a = p(&["-qv"]).unwrap();
|
||||
assert!(a.verbose && !a.quiet);
|
||||
let a = p(&["-vq"]).unwrap();
|
||||
assert!(a.quiet && !a.verbose);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_arguments_are_attached_only() {
|
||||
let a = p(&["-lfoo.log"]).unwrap();
|
||||
assert_eq!(a.log_summary, Some(Some("foo.log".to_string())));
|
||||
let a = p(&["-l", "foo.log"]).unwrap();
|
||||
assert_eq!(a.log_summary, Some(None));
|
||||
assert_eq!(a.span.as_deref(), Some("foo.log"));
|
||||
let a = p(&["--log-summary=bar.log"]).unwrap();
|
||||
assert_eq!(a.log_summary, Some(Some("bar.log".to_string())));
|
||||
let a = p(&["--log-summary", "bar.log"]).unwrap();
|
||||
assert_eq!(a.log_summary, Some(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn never_skip_modes() {
|
||||
let a = p(&["-z", "1"]).unwrap();
|
||||
assert_ne!(a.paranoia_mode & PARANOIA_MODE_NEVERSKIP, 0);
|
||||
assert_eq!(a.span.as_deref(), Some("1"));
|
||||
let a = p(&["-z5", "1"]).unwrap();
|
||||
assert_eq!(a.paranoia_mode & PARANOIA_MODE_NEVERSKIP, 0);
|
||||
assert_eq!(a.max_retries, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_of_options() {
|
||||
let a = p(&["--", "-3", "out.raw"]).unwrap();
|
||||
assert_eq!(a.span.as_deref(), Some("-3"));
|
||||
assert_eq!(a.outfile.as_deref(), Some("out.raw"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_formats_last_wins() {
|
||||
assert_eq!(
|
||||
p(&["-w", "-r", "1"]).unwrap().output_format,
|
||||
OutputFormat::RawLittle
|
||||
);
|
||||
assert_eq!(
|
||||
p(&["-p", "-a", "1"]).unwrap().output_format,
|
||||
OutputFormat::Aifc
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incompatible_devices() {
|
||||
assert!(p(&["-g", "/dev/sg0", "-k", "/dev/sr0", "1"]).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
std::process::exit(cdrustanoia_cli::run(&argv));
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
//! cdrustanoia command line driver, ported from cdparanoia 10.2 main.c.
|
||||
|
||||
mod analyze;
|
||||
pub mod args;
|
||||
mod log;
|
||||
mod output;
|
||||
mod progress;
|
||||
mod span;
|
||||
|
||||
use crate::log::Log;
|
||||
use crate::output::{write_header, BufferedWriter, OutputFormat};
|
||||
use crate::progress::Progress;
|
||||
use crate::span::parse_span;
|
||||
use args::Args;
|
||||
use cdrustanoia_core::device::discover::{find_a_cdrom, identify, identify_cooked, identify_sgio};
|
||||
use cdrustanoia_core::device::{host_big_endian, swap16, Drive};
|
||||
use cdrustanoia_core::paranoia::*;
|
||||
use cdrustanoia_core::{VERSION, VERSIONNUM};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, Write};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::path::Path;
|
||||
|
||||
const MANUAL: &str = r#"cdparanoia III release 10.2 (September 11, 2008)
|
||||
|
||||
(C) 2008 Monty <monty@xiph.org> and Xiph.Org
|
||||
|
||||
Report bugs to paranoia@xiph.org
|
||||
http://www.xiph.org/paranoia/
|
||||
USAGE:
|
||||
cdparanoia [options] <span> [outfile]
|
||||
|
||||
OPTIONS:
|
||||
-A --analyze-drive : run and log a complete analysis of drive
|
||||
caching, timing and reading behavior;
|
||||
verifies that cdparanoia is correctly
|
||||
modelling a sprcific drive's cache and
|
||||
read behavior. Implies -vQL
|
||||
-v --verbose : extra verbose operation
|
||||
-q --quiet : quiet operation
|
||||
-e --stderr-progress : force output of progress information to
|
||||
stderr (for wrapper scripts)
|
||||
-l --log-summary [<file>] : save result summary to file, default
|
||||
filename cdparanoia.log
|
||||
-L --log-debug [<file>] : save detailed device autosense and
|
||||
debugging output to file, default
|
||||
filename cdparanoia.log
|
||||
-V --version : print version info and quit
|
||||
-Q --query : autosense drive, query disc and quit
|
||||
-B --batch : 'batch' mode (saves each track to a
|
||||
seperate file.
|
||||
-s --search-for-drive : do an exhaustive search for drive
|
||||
-h --help : print help
|
||||
|
||||
-p --output-raw : output raw 16 bit PCM in host byte
|
||||
order
|
||||
-r --output-raw-little-endian : output raw 16 bit little-endian PCM
|
||||
-R --output-raw-big-endian : output raw 16 bit big-endian PCM
|
||||
-w --output-wav : output as WAV file (default)
|
||||
-f --output-aiff : output as AIFF file
|
||||
-a --output-aifc : output as AIFF-C file
|
||||
|
||||
-c --force-cdrom-little-endian : force treating drive as little endian
|
||||
-C --force-cdrom-big-endian : force treating drive as big endian
|
||||
-n --force-default-sectors <n> : force default number of sectors in read
|
||||
to n sectors
|
||||
-o --force-search-overlap <n> : force minimum overlap search during
|
||||
verification to n sectors
|
||||
-d --force-cdrom-device <dev> : use specified device; disallow
|
||||
autosense
|
||||
-k --force-cooked-device <dev> : use specified cdrom device and force
|
||||
use of the old 'cooked ioctl' kernel
|
||||
interface. -k cannot be used with -d
|
||||
or -g.
|
||||
-g --force-generic-device <dev> : use specified generic scsi device and
|
||||
force use of the old SG kernel
|
||||
interface. -g cannot be used with -k.
|
||||
-S --force-read-speed <n> : read from device at specified speed; by
|
||||
default, cdparanoia sets drive to full
|
||||
speed.
|
||||
-t --toc-offset <n> : Add <n> sectors to the values reported
|
||||
when addressing tracks. May be negative
|
||||
-T --toc-bias : Assume that the beginning offset of
|
||||
track 1 as reported in the TOC will be
|
||||
addressed as LBA 0. Necessary for some
|
||||
Toshiba drives to get track boundaries
|
||||
correct
|
||||
-O --sample-offset <n> : Add <n> samples to the offset when
|
||||
reading data. May be negative.
|
||||
-z --never-skip[=n] : never accept any less than perfect
|
||||
data reconstruction (don't allow 'V's)
|
||||
but if [n] is given, skip after [n]
|
||||
retries without progress.
|
||||
-Z --disable-paranoia : disable all paranoia checking
|
||||
-Y --disable-extra-paranoia : only do cdda2wav-style overlap checking
|
||||
-X --abort-on-skip : abort on imperfect reads/skips
|
||||
|
||||
OUTPUT SMILIES:
|
||||
:-) Normal operation, low/no jitter
|
||||
:-| Normal operation, considerable jitter
|
||||
:-/ Read drift
|
||||
:-P Unreported loss of streaming in atomic read operation
|
||||
8-| Finding read problems at same point during reread; hard to correct
|
||||
:-0 SCSI/ATAPI transport error
|
||||
:-( Scratch detected
|
||||
;-( Gave up trying to perform a correction
|
||||
8-X Aborted (as per -X) due to a scratch/skip
|
||||
:^D Finished extracting
|
||||
|
||||
PROGRESS BAR SYMBOLS:
|
||||
<space> No corrections needed
|
||||
- Jitter correction required
|
||||
+ Unreported loss of streaming/other error in read
|
||||
! Errors are getting through stage 1 but corrected in stage2
|
||||
e SCSI/ATAPI transport error (corrected)
|
||||
V Uncorrected error/skip
|
||||
|
||||
SPAN ARGUMENT:
|
||||
The span argument may be a simple track number or a offset/span
|
||||
specification. The syntax of an offset/span takes the rough form:
|
||||
|
||||
1[ww:xx:yy.zz]-2[aa:bb:cc.dd]
|
||||
|
||||
Here, 1 and 2 are track numbers; the numbers in brackets provide a
|
||||
finer grained offset within a particular track. [aa:bb:cc.dd] is in
|
||||
hours/minutes/seconds/sectors format. Zero fields need not be
|
||||
specified: [::20], [:20], [20], [20.], etc, would be interpreted as
|
||||
twenty seconds, [10:] would be ten minutes, [.30] would be thirty
|
||||
sectors (75 sectors per second).
|
||||
|
||||
When only a single offset is supplied, it is interpreted as a starting
|
||||
offset and ripping will continue to the end of he track. If a single
|
||||
offset is preceeded or followed by a hyphen, the implicit missing
|
||||
offset is taken to be the start or end of the disc, respectively. Thus:
|
||||
|
||||
1:[20.35] Specifies ripping from track 1, second 20, sector 35 to
|
||||
the end of track 1.
|
||||
|
||||
1:[20.35]- Specifies ripping from 1[20.35] to the end of the disc
|
||||
|
||||
-2 Specifies ripping from the beginning of the disc up to
|
||||
(and including) track 2
|
||||
|
||||
-2:[30.35] Specifies ripping from the beginning of the disc up to
|
||||
2:[30.35]
|
||||
|
||||
2-4 Specifies ripping from the beginning of track two to the
|
||||
end of track 4.
|
||||
|
||||
Don't forget to protect square brackets and preceeding hyphens from
|
||||
the shell...
|
||||
|
||||
A few examples, protected from the shell:
|
||||
A) query only with exhaustive search for a drive and full reporting
|
||||
of autosense:
|
||||
cdparanoia -vsQ
|
||||
|
||||
B) extract up to and including track 3, putting each track in a seperate
|
||||
file:
|
||||
cdparanoia -B -- "-3"
|
||||
|
||||
C) extract from track 1, time 0:30.12 to 1:10.00:
|
||||
cdparanoia "1[:30.12]-1[1:10]"
|
||||
|
||||
Submit bug reports to paranoia@xiph.org
|
||||
|
||||
"#;
|
||||
|
||||
fn usage(out: &mut dyn Write) {
|
||||
let _ = out.write_all(MANUAL.as_bytes());
|
||||
}
|
||||
|
||||
fn open_log_name(name: &str) -> Result<Box<dyn Write>, String> {
|
||||
if name == "-" {
|
||||
Ok(Box::new(io::stdout()))
|
||||
} else {
|
||||
OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.mode(0o666)
|
||||
.open(name)
|
||||
.map(|f| Box::new(f) as Box<dyn Write>)
|
||||
.map_err(|e| format!("Cannot open log file {}: {}", name, e))
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logs(args: &Args, argv: &[String]) -> Result<Log, String> {
|
||||
let mut log = Log::new();
|
||||
log.quiet = args.quiet;
|
||||
|
||||
let summary_name = args
|
||||
.log_summary
|
||||
.as_ref()
|
||||
.map(|v| v.clone().unwrap_or_else(|| "cdparanoia.log".to_string()));
|
||||
let report_name = args
|
||||
.log_debug
|
||||
.as_ref()
|
||||
.map(|v| v.clone().unwrap_or_else(|| "cdparanoia.log".to_string()));
|
||||
|
||||
match (&summary_name, &report_name) {
|
||||
(Some(s), Some(r)) if s == r && s != "-" => {
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.mode(0o666)
|
||||
.open(s)
|
||||
.map_err(|e| format!("Cannot open log file {}: {}", s, e))?;
|
||||
let clone = file
|
||||
.try_clone()
|
||||
.map_err(|e| format!("Cannot duplicate log file {}: {}", s, e))?;
|
||||
log.set_summary_writer(Box::new(file));
|
||||
log.set_report_writer(Box::new(clone));
|
||||
}
|
||||
_ => {
|
||||
if let Some(name) = &summary_name {
|
||||
log.set_summary_writer(open_log_name(name)?);
|
||||
}
|
||||
if let Some(name) = &report_name {
|
||||
log.set_report_writer(open_log_name(name)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if log.summary_file.is_some() {
|
||||
let line = argv.join(" ");
|
||||
log.summary(format_args!("{}", line));
|
||||
if summary_name.as_deref() != report_name.as_deref() {
|
||||
log.summary(format_args!("{}\n", VERSION));
|
||||
log.summary(format_args!("Using cdda library version: {}", VERSIONNUM));
|
||||
log.summary(format_args!(
|
||||
"Using paranoia library version: {}",
|
||||
VERSIONNUM
|
||||
));
|
||||
}
|
||||
}
|
||||
if log.report_file.is_some() && summary_name.as_deref() != report_name.as_deref() {
|
||||
let line = argv.join(" ");
|
||||
log.report(format_args!("{}", line));
|
||||
}
|
||||
log.flush();
|
||||
Ok(log)
|
||||
}
|
||||
|
||||
fn display_toc(d: &dyn Drive, log: &mut Log) {
|
||||
let mut s = String::new();
|
||||
s.push_str("\nTable of contents (audio tracks only):\n");
|
||||
s.push_str("track length begin copy pre ch\n");
|
||||
s.push_str("===========================================================");
|
||||
let mut audiolen = 0i64;
|
||||
for i in 1..=d.tracks() {
|
||||
if d.track_audiop(i) {
|
||||
let sec = d.track_firstsector(i);
|
||||
let off = d.track_lastsector(i) - sec + 1;
|
||||
s.push_str(&format!(
|
||||
"\n{:3}. {:7} [{:02}:{:02}.{:02}] {:7} [{:02}:{:02}.{:02}] {} {} {}",
|
||||
i,
|
||||
off,
|
||||
off / (60 * 75),
|
||||
(off / 75) % 60,
|
||||
off % 75,
|
||||
sec,
|
||||
sec / (60 * 75),
|
||||
(sec / 75) % 60,
|
||||
sec % 75,
|
||||
if d.track_copyp(i) { " OK" } else { " no" },
|
||||
if d.track_preemp(i) { " yes" } else { " no" },
|
||||
if d.track_channels(i) == 2 { " 2" } else { " 4" }
|
||||
));
|
||||
audiolen += off;
|
||||
}
|
||||
}
|
||||
s.push_str(&format!(
|
||||
"\nTOTAL {:7} [{:02}:{:02}.{:02}] (audio only)",
|
||||
audiolen,
|
||||
audiolen / (60 * 75),
|
||||
(audiolen / 75) % 60,
|
||||
audiolen % 75
|
||||
));
|
||||
log.report(format_args!("{}", s));
|
||||
log.report(format_args!(" "));
|
||||
}
|
||||
|
||||
fn read_sector(
|
||||
p: &mut Paranoia,
|
||||
progress: &mut Progress,
|
||||
log: &mut Log,
|
||||
max_retries: i32,
|
||||
) -> Option<Vec<i16>> {
|
||||
let mut callback = |pos: i64, func: i32| progress.callback(pos, func, log);
|
||||
p.read_limited(&mut callback, max_retries)
|
||||
.map(|s| s.to_vec())
|
||||
}
|
||||
|
||||
fn samples_as_bytes(samples: &[i16]) -> &[u8] {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
samples.as_ptr() as *const u8,
|
||||
std::mem::size_of_val(samples),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn make_output_name(
|
||||
template: &Option<String>,
|
||||
batch: bool,
|
||||
track: i32,
|
||||
format: OutputFormat,
|
||||
) -> Option<String> {
|
||||
match template {
|
||||
None => {
|
||||
if batch {
|
||||
Some(format!("track{:02}.{}", track, format.default_extension()))
|
||||
} else {
|
||||
Some(format.default_extension().to_string())
|
||||
}
|
||||
}
|
||||
Some(name) if name == "-" => None,
|
||||
Some(name) => {
|
||||
let (prefix, file) = match name.rfind('/') {
|
||||
Some(pos) => (&name[..pos + 1], &name[pos + 1..]),
|
||||
None => ("", name.as_str()),
|
||||
};
|
||||
let mut out = if batch {
|
||||
format!("{}track{:02}.{}", prefix, track, file)
|
||||
} else {
|
||||
format!("{}{}", prefix, file)
|
||||
};
|
||||
if file.is_empty() {
|
||||
out.push_str(format.default_extension());
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_output(name: Option<&str>, template_stdout: bool) -> Result<File, String> {
|
||||
if template_stdout {
|
||||
let fd = unsafe { libc::dup(libc::STDOUT_FILENO) };
|
||||
if fd < 0 {
|
||||
return Err("Cannot duplicate stdout".to_string());
|
||||
}
|
||||
return Ok(unsafe { File::from_raw_fd(fd) });
|
||||
}
|
||||
let name = name.ok_or_else(|| "Invalid output file".to_string())?;
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o666)
|
||||
.open(name)
|
||||
.map_err(|e| format!("Cannot open specified output file {}: {}", name, e))
|
||||
}
|
||||
|
||||
pub fn run(argv: &[String]) -> i32 {
|
||||
let args = match args::parse(argv) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
usage(&mut io::stderr());
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
if args.help {
|
||||
usage(&mut io::stdout());
|
||||
return 0;
|
||||
}
|
||||
if args.version {
|
||||
eprintln!("{}\n", VERSION);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut log = match setup_logs(&args, argv) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
let span_arg = args.span.clone();
|
||||
if span_arg.is_none() && !args.query && !args.batch {
|
||||
usage(&mut io::stderr());
|
||||
let _ = io::stderr().flush();
|
||||
return 1;
|
||||
}
|
||||
|
||||
log.report(format_args!("{}\n", VERSION));
|
||||
if args.verbose {
|
||||
log.report(format_args!("Using cdda library version: {}", VERSIONNUM));
|
||||
log.report(format_args!(
|
||||
"Using paranoia library version: {}",
|
||||
VERSIONNUM
|
||||
));
|
||||
}
|
||||
|
||||
let drive_result: Result<Box<dyn Drive>, String> = if let Some(dev) = &args.force_cooked_device
|
||||
{
|
||||
identify_cooked(dev, args.verbose)
|
||||
} else if let Some(dev) = &args.force_generic_device {
|
||||
match identify_sgio(dev, args.verbose) {
|
||||
Ok(d) => Ok(d),
|
||||
Err(e) => {
|
||||
if let Some(spec) = &args.force_device {
|
||||
identify(spec, args.verbose, false).map_err(|_| e)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(dev) = &args.force_device {
|
||||
identify(dev, args.verbose, false)
|
||||
} else if args.search {
|
||||
find_a_cdrom(args.verbose).ok_or_else(|| "No cdrom drives found".to_string())
|
||||
} else if Path::new("/dev/cdrom").symlink_metadata().is_err() {
|
||||
find_a_cdrom(args.verbose).ok_or_else(|| "No cdrom drives found".to_string())
|
||||
} else {
|
||||
match identify("/dev/cdrom", args.verbose, false) {
|
||||
Ok(d) => Ok(d),
|
||||
Err(_) => {
|
||||
if !args.verbose {
|
||||
log.report_c(format_args!(
|
||||
"\n/dev/cdrom exists but isn't accessible. By default,\n\
|
||||
cdparanoia stops searching for an accessible drive here.\n\
|
||||
Consider using -sv to force a more complete autosense\n\
|
||||
of the machine.\n\nMore information about /dev/cdrom:"
|
||||
));
|
||||
let _ = identify("/dev/cdrom", true, false);
|
||||
log.report(format_args!("\n"));
|
||||
return 1;
|
||||
}
|
||||
Err("Unable to open cdrom drive".to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut drive = match drive_result {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
if !args.verbose {
|
||||
log.report(format_args!(
|
||||
"\nUnable to open cdrom drive; -v will give more information."
|
||||
));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = drive.open_disc() {
|
||||
if args.verbose {
|
||||
log.report(format_args!("\nDisc open failed: {}", e));
|
||||
}
|
||||
log.report(format_args!(
|
||||
"\nUnable to open disc. Is there an audio CD in the drive?"
|
||||
));
|
||||
return 1;
|
||||
}
|
||||
if !drive.opened() || drive.tracks() < 1 {
|
||||
log.report(format_args!(
|
||||
"\nUnable to open disc. Is there an audio CD in the drive?"
|
||||
));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if let Some(endian) = args.force_endian {
|
||||
drive.set_bigendianp(endian);
|
||||
log.report(format_args!(
|
||||
"Forcing CDROM sense to {}-endian; ignoring preset and autosense",
|
||||
if endian == 0 { "little" } else { "big" }
|
||||
));
|
||||
}
|
||||
if let Some(sectors) = args.force_sectors {
|
||||
if !(1..=100).contains(§ors) {
|
||||
log.report(format_args!(
|
||||
"Default sector read size must be 1<= n <= 100"
|
||||
));
|
||||
return 1;
|
||||
}
|
||||
log.report(format_args!(
|
||||
"Forcing default to read {} sectors; ignoring preset and autosense",
|
||||
sectors
|
||||
));
|
||||
drive.set_nsectors(sectors as i64);
|
||||
}
|
||||
if let Some(overlap) = args.force_overlap {
|
||||
if !(0..=75).contains(&overlap) {
|
||||
log.report(format_args!("Search overlap sectors must be 0<= n <=75"));
|
||||
return 1;
|
||||
}
|
||||
log.report(format_args!(
|
||||
"Forcing search overlap to {} sectors; ignoring autosense",
|
||||
overlap
|
||||
));
|
||||
}
|
||||
|
||||
let speed = match args.force_speed {
|
||||
Some(0) | None => -1,
|
||||
Some(v) => v,
|
||||
};
|
||||
if speed != -1 {
|
||||
log.report(format_args!("\nAttempting to set speed to {}x... ", speed));
|
||||
} else if args.verbose {
|
||||
log.report(format_args!("\nAttempting to set cdrom to full speed... "));
|
||||
}
|
||||
if drive.speed_set(speed) != 0 {
|
||||
if args.verbose || speed != -1 {
|
||||
log.report(format_args!(
|
||||
"\tCDROM speed set FAILED. Continuing anyway..."
|
||||
));
|
||||
}
|
||||
} else if args.verbose {
|
||||
log.report(format_args!("\tdrive returned OK."));
|
||||
}
|
||||
|
||||
if args.analyze {
|
||||
let warn = analyze::analyze_cache(drive.as_mut(), &mut log, speed);
|
||||
if warn == 0 {
|
||||
log.report_c(format_args!("\nDrive tests OK with Paranoia.\n\n"));
|
||||
return 0;
|
||||
}
|
||||
log.report_c(format_args!(
|
||||
"\nWARNING! PARANOIA MAY NOT BE TRUSTWORTHY WITH THIS DRIVE!\n\
|
||||
\nThe Paranoia library may not model this CDROM drive's cache\
|
||||
\ncorrectly according to this analysis run. Analysis is not\
|
||||
\nalways accurate (it can be fooled by machine load or random\
|
||||
\nkernel latencies), but if a failed result happens more often\
|
||||
\nthan one time in twenty on an unloaded machine, please mail\
|
||||
\nthe cdparanoia.log file produced by this failed analysis to\
|
||||
\nparanoia-dev@xiph.org to assist developers in extending\
|
||||
\nParanoia to handle this CDROM properly.\n\n"
|
||||
));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if args.query || args.verbose {
|
||||
display_toc(drive.as_ref(), &mut log);
|
||||
}
|
||||
if args.query {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut toc_offset = args.toc_offset as i64;
|
||||
let mut sample_offset = args.sample_offset;
|
||||
if sample_offset != 0 {
|
||||
toc_offset += (sample_offset / 588) as i64;
|
||||
sample_offset %= 588;
|
||||
if sample_offset < 0 {
|
||||
sample_offset += 588;
|
||||
toc_offset -= 1;
|
||||
}
|
||||
}
|
||||
if args.toc_bias {
|
||||
toc_offset = -drive.track_firstsector(1);
|
||||
}
|
||||
{
|
||||
let tracks = drive.tracks();
|
||||
let toc = drive.toc_mut();
|
||||
for i in 0..=tracks as usize {
|
||||
toc.entries[i].start_sector += toc_offset;
|
||||
}
|
||||
}
|
||||
|
||||
if drive.nsectors() == 1 {
|
||||
log.report(format_args!(
|
||||
"WARNING: The autosensed/selected sectors per read value is\n\
|
||||
one sector, making it very unlikely Paranoia can \n\
|
||||
work.\n\n\
|
||||
Attempting to continue...\n"
|
||||
));
|
||||
}
|
||||
|
||||
let (first_sector, last_sector) = match &span_arg {
|
||||
Some(span) => match parse_span(drive.toc(), span) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log.report(format_args!("{}", e));
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
None => (drive.disc_firstsector(), drive.disc_lastsector()),
|
||||
};
|
||||
|
||||
let track1 = drive.sector_gettrack(first_sector);
|
||||
let track2 = drive.sector_gettrack(last_sector);
|
||||
if track1 < 0 || track2 < 0 {
|
||||
log.report(format_args!("Error parsing span argument"));
|
||||
return 1;
|
||||
}
|
||||
for i in track1..=track2 {
|
||||
if !drive.track_audiop(i) {
|
||||
log.report(format_args!(
|
||||
"Selected span contains non audio tracks. Aborting.\n"
|
||||
));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
let off1 = first_sector - drive.track_firstsector(track1);
|
||||
let off2 = last_sector - drive.track_firstsector(track2);
|
||||
log.report(format_args!(
|
||||
"Ripping from sector {:7} (track {:2} [{}:{:02}.{:02}])\n\t to sector {:7} (track {:2} [{}:{:02}.{:02}])",
|
||||
first_sector,
|
||||
track1,
|
||||
off1 / (60 * 75),
|
||||
(off1 / 75) % 60,
|
||||
off1 % 75,
|
||||
last_sector,
|
||||
track2,
|
||||
off2 / (60 * 75),
|
||||
(off2 / 75) % 60,
|
||||
off2 % 75
|
||||
));
|
||||
|
||||
let mut p = Paranoia::new(drive);
|
||||
p.modeset(args.paranoia_mode);
|
||||
if let Some(overlap) = args.force_overlap {
|
||||
p.overlapset(overlap as i64);
|
||||
}
|
||||
p.seek(first_sector, 0);
|
||||
|
||||
unsafe {
|
||||
libc::seteuid(libc::getuid());
|
||||
libc::setegid(libc::getgid());
|
||||
}
|
||||
|
||||
if sample_offset != 0 {
|
||||
let tracks = p.drive().tracks() as usize;
|
||||
if let Some(e) = p.drive_mut().toc_mut().entries.get_mut(tracks) {
|
||||
e.start_sector += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let host_big = host_big_endian();
|
||||
let target_big = args.output_format.output_big_endian(host_big);
|
||||
let mut offset_buffer = [0u8; 2352];
|
||||
let mut offset_buffer_used = 0usize;
|
||||
let mut offset_skip = (sample_offset * 4) as usize;
|
||||
let mut cursor = first_sector;
|
||||
let mut progress = Progress::default();
|
||||
progress.callscript = args.stderr_progress;
|
||||
progress.abort_on_skip = args.abort_on_skip;
|
||||
|
||||
while cursor <= last_sector {
|
||||
let (batch_first, batch_last, batch_track) = if args.batch {
|
||||
let bt = p.drive().sector_gettrack(cursor);
|
||||
if bt < 0 {
|
||||
break;
|
||||
}
|
||||
let mut bl = p.drive().track_lastsector(bt);
|
||||
if bl > last_sector {
|
||||
bl = last_sector;
|
||||
}
|
||||
(cursor, bl, bt)
|
||||
} else {
|
||||
(first_sector, last_sector, -1)
|
||||
};
|
||||
|
||||
progress.callbegin = batch_first;
|
||||
progress.callend = batch_last;
|
||||
progress.reset_batch();
|
||||
|
||||
let template_stdout = args.outfile.as_deref() == Some("-");
|
||||
let outfile_name =
|
||||
make_output_name(&args.outfile, args.batch, batch_track, args.output_format);
|
||||
if template_stdout {
|
||||
if args.batch {
|
||||
log.report(format_args!(
|
||||
"Are you sure you wanted 'batch' (-B) output with stdout?"
|
||||
));
|
||||
}
|
||||
log.report(format_args!("outputting to stdout"));
|
||||
} else {
|
||||
log.report(format_args!(
|
||||
"outputting to {}",
|
||||
outfile_name.as_deref().unwrap_or("")
|
||||
));
|
||||
}
|
||||
if let Some(name) = &outfile_name {
|
||||
log.summary(format_args!("outputting to {}", name));
|
||||
} else {
|
||||
log.summary(format_args!("outputting to stdout"));
|
||||
}
|
||||
|
||||
let file = match open_output(outfile_name.as_deref(), template_stdout) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
log.report(format_args!("{}", e));
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
let mut out = BufferedWriter::new(file);
|
||||
let bytes = ((batch_last - batch_first + 1) * 2352) as u32;
|
||||
if let Err(e) = write_header(&mut out, args.output_format, bytes) {
|
||||
log.report(format_args!("Error writing output: {}", e));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if offset_buffer_used > 0 {
|
||||
cursor += 1;
|
||||
if let Err(e) = out.write_all(&offset_buffer[offset_buffer_used..]) {
|
||||
log.report(format_args!("Error writing output: {}", e));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
progress.skipped_flag = false;
|
||||
while cursor <= batch_last {
|
||||
let sector = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
progress.skipped_flag = true;
|
||||
log.report(format_args!(
|
||||
"\nparanoia_read: Unrecoverable error, bailing."
|
||||
));
|
||||
break;
|
||||
}
|
||||
};
|
||||
if progress.skipped_flag && args.abort_on_skip {
|
||||
cursor = batch_last + 1;
|
||||
break;
|
||||
}
|
||||
progress.skipped_flag = false;
|
||||
cursor += 1;
|
||||
|
||||
let mut data = sector;
|
||||
if target_big != host_big {
|
||||
for s in &mut data {
|
||||
*s = swap16(*s);
|
||||
}
|
||||
}
|
||||
progress.callback(cursor * 1176 - 1, -2, &mut log);
|
||||
|
||||
let data_bytes = samples_as_bytes(&data);
|
||||
if let Err(e) = out.write_all(&data_bytes[offset_skip.min(data_bytes.len())..]) {
|
||||
log.report(format_args!("Error writing output: {}", e));
|
||||
return 1;
|
||||
}
|
||||
offset_skip = 0;
|
||||
|
||||
if sample_offset != 0 && cursor > batch_last {
|
||||
let extra = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
progress.skipped_flag = true;
|
||||
log.report(format_args!(
|
||||
"\nparanoia_read: Unrecoverable error reading through \n\
|
||||
\t\t\tsample_offset shift\n\tat end of track, bailing."
|
||||
));
|
||||
break;
|
||||
}
|
||||
};
|
||||
if progress.skipped_flag && args.abort_on_skip {
|
||||
break;
|
||||
}
|
||||
progress.skipped_flag = false;
|
||||
let mut extra = extra;
|
||||
if target_big != host_big {
|
||||
for s in &mut extra {
|
||||
*s = swap16(*s);
|
||||
}
|
||||
}
|
||||
let eb = samples_as_bytes(&extra);
|
||||
offset_buffer[..eb.len().min(2352)].copy_from_slice(&eb[..eb.len().min(2352)]);
|
||||
offset_buffer_used = (sample_offset as usize * 4).min(2352);
|
||||
progress.callback(cursor * 1176, -2, &mut log);
|
||||
if let Err(e) = out.write_all(&offset_buffer[..offset_buffer_used]) {
|
||||
log.report(format_args!("Error writing output: {}", e));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progress.callback(cursor * 1176 - 1, -1, &mut log);
|
||||
if let Err(e) = out.close() {
|
||||
log.report(format_args!("Error writing output: {}", e));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if progress.skipped_flag {
|
||||
if let Some(name) = &outfile_name {
|
||||
log.report(format_args!("\nRemoving aborted file: {}", name));
|
||||
let _ = std::fs::remove_file(name);
|
||||
}
|
||||
if batch_track != -1 {
|
||||
let next_track = batch_track + 1;
|
||||
let next = p.drive().track_firstsector(next_track);
|
||||
if next >= 0 {
|
||||
cursor = next;
|
||||
p.seek(cursor, 0);
|
||||
} else {
|
||||
cursor = last_sector + 1;
|
||||
}
|
||||
offset_skip = (sample_offset * 4) as usize;
|
||||
offset_buffer_used = 0;
|
||||
}
|
||||
}
|
||||
log.report(format_args!("\n"));
|
||||
}
|
||||
|
||||
log.report(format_args!("Done.\n\n"));
|
||||
log.flush();
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Report/log destination handling, matching report.h's macros plus the
|
||||
//! separate summary and debug streams from main.c.
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub struct Log {
|
||||
pub quiet: bool,
|
||||
pub summary_file: Option<Box<dyn Write>>,
|
||||
pub report_file: Option<Box<dyn Write>>,
|
||||
}
|
||||
|
||||
impl Default for Log {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Log {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
quiet: false,
|
||||
summary_file: None,
|
||||
report_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_summary_writer(&mut self, writer: Box<dyn Write>) {
|
||||
self.summary_file = Some(writer);
|
||||
}
|
||||
|
||||
pub fn set_report_writer(&mut self, writer: Box<dyn Write>) {
|
||||
self.report_file = Some(writer);
|
||||
}
|
||||
|
||||
pub fn report(&mut self, args: std::fmt::Arguments<'_>) {
|
||||
if !self.quiet {
|
||||
let _ = writeln!(io::stderr(), "{}", args);
|
||||
}
|
||||
if let Some(f) = self.report_file.as_mut() {
|
||||
let _ = writeln!(f, "{}", args);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_c(&mut self, args: std::fmt::Arguments<'_>) {
|
||||
if !self.quiet {
|
||||
let _ = write!(io::stderr(), "{}", args);
|
||||
}
|
||||
if let Some(f) = self.report_file.as_mut() {
|
||||
let _ = write!(f, "{}", args);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_c(&mut self, args: std::fmt::Arguments<'_>) {
|
||||
if !self.quiet {
|
||||
let _ = write!(io::stderr(), "{}", args);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_c(&mut self, args: std::fmt::Arguments<'_>) {
|
||||
if let Some(f) = self.report_file.as_mut() {
|
||||
let _ = write!(f, "{}", args);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary(&mut self, args: std::fmt::Arguments<'_>) {
|
||||
if let Some(f) = self.summary_file.as_mut() {
|
||||
let _ = writeln!(f, "{}", args);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_summary_bytes(&mut self, data: &[u8]) {
|
||||
if let Some(f) = self.summary_file.as_mut() {
|
||||
let _ = f.write_all(data);
|
||||
let _ = f.flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) {
|
||||
if let Some(f) = self.summary_file.as_mut() {
|
||||
let _ = f.flush();
|
||||
}
|
||||
if let Some(f) = self.report_file.as_mut() {
|
||||
let _ = f.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! report {
|
||||
($log:expr, $($arg:tt)*) => {
|
||||
$log.report(format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! report_c {
|
||||
($log:expr, $($arg:tt)*) => {
|
||||
$log.report_c(format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print_c {
|
||||
($log:expr, $($arg:tt)*) => {
|
||||
$log.print_c(format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_c {
|
||||
($log:expr, $($arg:tt)*) => {
|
||||
$log.log_c(format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! summary {
|
||||
($log:expr, $($arg:tt)*) => {
|
||||
$log.summary(format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
std::process::exit(cdrustanoia_cli::run(&argv));
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//! WAV/AIFF/AIFC headers and buffered output, ported from header.c and
|
||||
//! buffering_write.c.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub const OUTBUF_SZ: usize = 32 * 1024;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum OutputFormat {
|
||||
RawHost,
|
||||
RawLittle,
|
||||
RawBig,
|
||||
Wav,
|
||||
Aiff,
|
||||
Aifc,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
pub fn default_extension(self) -> &'static str {
|
||||
match self {
|
||||
OutputFormat::RawHost | OutputFormat::RawLittle | OutputFormat::RawBig => "cdda.raw",
|
||||
OutputFormat::Wav => "cdda.wav",
|
||||
OutputFormat::Aifc => "cdda.aifc",
|
||||
OutputFormat::Aiff => "cdda.aiff",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_extension(self) -> &'static str {
|
||||
match self {
|
||||
OutputFormat::RawHost | OutputFormat::RawLittle | OutputFormat::RawBig => "raw",
|
||||
OutputFormat::Wav => "wav",
|
||||
OutputFormat::Aifc => "aifc",
|
||||
OutputFormat::Aiff => "aiff",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_big_endian(self, host_big: bool) -> bool {
|
||||
match self {
|
||||
OutputFormat::RawHost => host_big,
|
||||
OutputFormat::RawLittle | OutputFormat::Wav => false,
|
||||
OutputFormat::RawBig | OutputFormat::Aiff | OutputFormat::Aifc => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn put_num<W: Write>(w: &mut W, num: u32, big_endian: bool, bytes: usize) -> io::Result<()> {
|
||||
let mut tmp = [0u8; 4];
|
||||
if big_endian {
|
||||
let b = num.to_be_bytes();
|
||||
for (i, slot) in tmp.iter_mut().take(bytes).enumerate() {
|
||||
*slot = b[4 - bytes + i];
|
||||
}
|
||||
} else {
|
||||
let b = num.to_le_bytes();
|
||||
tmp[..bytes].copy_from_slice(&b[..bytes]);
|
||||
}
|
||||
w.write_all(&tmp[..bytes])
|
||||
}
|
||||
|
||||
pub fn write_wav<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
|
||||
w.write_all(b"RIFF")?;
|
||||
put_num(w, bytes.wrapping_add(44).wrapping_sub(8), false, 4)?;
|
||||
w.write_all(b"WAVEfmt ")?;
|
||||
put_num(w, 16, false, 4)?;
|
||||
put_num(w, 1, false, 2)?;
|
||||
put_num(w, 2, false, 2)?;
|
||||
put_num(w, 44100, false, 4)?;
|
||||
put_num(w, 44100 * 2 * 2, false, 4)?;
|
||||
put_num(w, 4, false, 2)?;
|
||||
put_num(w, 16, false, 2)?;
|
||||
w.write_all(b"data")?;
|
||||
put_num(w, bytes, false, 4)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_aiff<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
|
||||
let frames = bytes / 4;
|
||||
w.write_all(b"FORM")?;
|
||||
put_num(w, bytes.wrapping_add(54).wrapping_sub(8), true, 4)?;
|
||||
w.write_all(b"AIFF")?;
|
||||
w.write_all(b"COMM")?;
|
||||
put_num(w, 18, true, 4)?;
|
||||
put_num(w, 2, true, 2)?;
|
||||
put_num(w, frames, true, 4)?;
|
||||
put_num(w, 16, true, 2)?;
|
||||
w.write_all(&[0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0])?;
|
||||
w.write_all(b"SSND")?;
|
||||
put_num(w, bytes.wrapping_add(8), true, 4)?;
|
||||
put_num(w, 0, true, 4)?;
|
||||
put_num(w, 0, true, 4)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_aifc<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
|
||||
let frames = bytes / 4;
|
||||
w.write_all(b"FORM")?;
|
||||
put_num(w, bytes.wrapping_add(86).wrapping_sub(8), true, 4)?;
|
||||
w.write_all(b"AIFC")?;
|
||||
w.write_all(b"FVER")?;
|
||||
put_num(w, 4, true, 4)?;
|
||||
put_num(w, 2726318400, true, 4)?;
|
||||
w.write_all(b"COMM")?;
|
||||
put_num(w, 38, true, 4)?;
|
||||
put_num(w, 2, true, 2)?;
|
||||
put_num(w, frames, true, 4)?;
|
||||
put_num(w, 16, true, 2)?;
|
||||
w.write_all(&[0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0])?;
|
||||
w.write_all(b"NONE")?;
|
||||
put_num(w, 14, true, 1)?;
|
||||
w.write_all(b"not compressed")?;
|
||||
put_num(w, 0, true, 1)?;
|
||||
w.write_all(b"SSND")?;
|
||||
put_num(w, bytes.wrapping_add(8), true, 4)?;
|
||||
put_num(w, 0, true, 4)?;
|
||||
put_num(w, 0, true, 4)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_header<W: Write>(w: &mut W, format: OutputFormat, bytes: u32) -> io::Result<()> {
|
||||
match format {
|
||||
OutputFormat::Wav => write_wav(w, bytes),
|
||||
OutputFormat::Aiff => write_aiff(w, bytes),
|
||||
OutputFormat::Aifc => write_aifc(w, bytes),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Buffered writer matching the original's 32 KiB write coalescing.
|
||||
pub struct BufferedWriter {
|
||||
file: File,
|
||||
buf: Vec<u8>,
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
impl BufferedWriter {
|
||||
pub fn new(file: File) -> Self {
|
||||
Self {
|
||||
file,
|
||||
buf: Vec::with_capacity(OUTBUF_SZ),
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_all(&mut self, mut data: &[u8]) -> io::Result<()> {
|
||||
while !data.is_empty() {
|
||||
let space = OUTBUF_SZ.saturating_sub(self.buf.len());
|
||||
if space == 0 {
|
||||
self.flush_buf()?;
|
||||
continue;
|
||||
}
|
||||
let n = space.min(data.len());
|
||||
self.buf.extend_from_slice(&data[..n]);
|
||||
data = &data[n..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush_buf(&mut self) -> io::Result<()> {
|
||||
if !self.buf.is_empty() {
|
||||
self.file.write_all(&self.buf)?;
|
||||
self.buf.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
self.flush_buf()?;
|
||||
self.file.flush()
|
||||
}
|
||||
|
||||
pub fn close(mut self) -> io::Result<()> {
|
||||
self.closed = true;
|
||||
self.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for BufferedWriter {
|
||||
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
|
||||
self.write_all(data)?;
|
||||
Ok(data.len())
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
BufferedWriter::flush(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BufferedWriter {
|
||||
fn drop(&mut self) {
|
||||
if !self.closed {
|
||||
let _ = self.flush_buf();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wav_header_size() {
|
||||
let mut v = Vec::new();
|
||||
write_wav(&mut v, 2352).unwrap();
|
||||
assert_eq!(v.len(), 44);
|
||||
assert_eq!(&v[..4], b"RIFF");
|
||||
assert_eq!(&v[8..16], b"WAVEfmt ");
|
||||
assert_eq!(&v[36..40], b"data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aiff_header_size() {
|
||||
let mut v = Vec::new();
|
||||
write_aiff(&mut v, 2352).unwrap();
|
||||
assert_eq!(v.len(), 54);
|
||||
assert_eq!(&v[..4], b"FORM");
|
||||
assert_eq!(&v[8..12], b"AIFF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aifc_header_size() {
|
||||
let mut v = Vec::new();
|
||||
write_aifc(&mut v, 2352).unwrap();
|
||||
assert_eq!(v.len(), 86);
|
||||
assert_eq!(&v[8..12], b"AIFC");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
//! Callback and progress display, ported from the callback in main.c.
|
||||
|
||||
use crate::log::Log;
|
||||
use cdrustanoia_core::callback::*;
|
||||
use cdrustanoia_core::device::CD_FRAMEWORDS;
|
||||
use std::io::{self, Write};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub const CALLBACK_STRINGS: [&str; 16] = [
|
||||
"wrote",
|
||||
"finished",
|
||||
"read",
|
||||
"verify",
|
||||
"jitter",
|
||||
"correction",
|
||||
"scratch",
|
||||
"scratch repair",
|
||||
"skip",
|
||||
"drift",
|
||||
"backoff",
|
||||
"overlap",
|
||||
"dropped",
|
||||
"duped",
|
||||
"transport error",
|
||||
"cache error",
|
||||
];
|
||||
|
||||
const GRAPH: usize = 30;
|
||||
|
||||
pub struct Progress {
|
||||
pub callbegin: i64,
|
||||
pub callend: i64,
|
||||
pub callscript: bool,
|
||||
pub abort_on_skip: bool,
|
||||
pub skipped_flag: bool,
|
||||
c_sector: i64,
|
||||
v_sector: i64,
|
||||
dispcache: [u8; GRAPH],
|
||||
last: i32,
|
||||
lasttime: i64,
|
||||
heartbeat: u8,
|
||||
slevel: i32,
|
||||
slast: i32,
|
||||
stimeout: i32,
|
||||
cacheerr: i32,
|
||||
overlap: i32,
|
||||
printit: i32,
|
||||
}
|
||||
|
||||
impl Default for Progress {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
callbegin: 0,
|
||||
callend: 0,
|
||||
callscript: false,
|
||||
abort_on_skip: false,
|
||||
skipped_flag: false,
|
||||
c_sector: 0,
|
||||
v_sector: 0,
|
||||
dispcache: [b' '; GRAPH],
|
||||
last: 0,
|
||||
lasttime: 0,
|
||||
heartbeat: b' ',
|
||||
slevel: 0,
|
||||
slast: 0,
|
||||
stimeout: 0,
|
||||
cacheerr: 0,
|
||||
overlap: 0,
|
||||
printit: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Progress {
|
||||
pub fn reset_batch(&mut self) {
|
||||
self.c_sector = 0;
|
||||
self.v_sector = 0;
|
||||
self.dispcache = [b' '; GRAPH];
|
||||
self.last = 0;
|
||||
self.lasttime = 0;
|
||||
self.heartbeat = b' ';
|
||||
self.slevel = 0;
|
||||
self.slast = 0;
|
||||
self.stimeout = 0;
|
||||
self.overlap = 0;
|
||||
self.skipped_flag = false;
|
||||
}
|
||||
|
||||
fn now_tenths() -> i64 {
|
||||
match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(d) => d.as_secs() as i64 * 10 + (d.subsec_micros() / 100_000) as i64,
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn callback(&mut self, inpos: i64, function: i32, log: &mut Log) {
|
||||
if self.callscript {
|
||||
let name = if (-2..=13).contains(&function) {
|
||||
CALLBACK_STRINGS[(function + 2) as usize]
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let _ = writeln!(io::stderr(), "##: {} [{}] @ {}", function, name, inpos);
|
||||
} else if function == PARANOIA_CB_CACHEERR {
|
||||
if self.cacheerr == 0 {
|
||||
let _ = write!(
|
||||
io::stderr(),
|
||||
"\rWARNING: The CDROM drive appears to be seeking impossibly quickly.\n\
|
||||
This could be due to timer bugs, a drive that really is improbably fast,\n\
|
||||
or, most likely, a bug in cdparanoia's cache modelling.\n\n\
|
||||
Please consider using the -A option to perform an analysis run, then mail\n\
|
||||
the cdparanoia.log file produced by the analysis to paranoia-dev@xiph.org\n\
|
||||
to assist developers in correcting the problem.\n\n"
|
||||
);
|
||||
}
|
||||
self.cacheerr += 1;
|
||||
}
|
||||
|
||||
if function == PARANOIA_CB_SKIP {
|
||||
// Keep -X functional even when progress is redirected or quiet.
|
||||
self.skipped_flag = true;
|
||||
}
|
||||
|
||||
if log.quiet {
|
||||
return;
|
||||
}
|
||||
|
||||
let osector = inpos;
|
||||
let sector = inpos / CD_FRAMEWORDS;
|
||||
|
||||
if self.printit == -1 {
|
||||
self.printit = if unsafe { libc::isatty(2) } == 1 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
}
|
||||
|
||||
if self.printit == 1 {
|
||||
let position = if self.callend != self.callbegin {
|
||||
((sector - self.callbegin) as f64 / (self.callend - self.callbegin) as f64
|
||||
* GRAPH as f64) as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let aheadposition = if self.callend != self.callbegin {
|
||||
((self.c_sector - self.callbegin) as f64 / (self.callend - self.callbegin) as f64
|
||||
* GRAPH as f64) as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if function == -2 {
|
||||
self.v_sector = sector;
|
||||
return;
|
||||
}
|
||||
if function == -1 {
|
||||
self.last = 8;
|
||||
self.heartbeat = b'*';
|
||||
self.slevel = 0;
|
||||
self.v_sector = sector;
|
||||
} else if position < GRAPH as i32 && position >= 0 {
|
||||
let p = position as usize;
|
||||
match function {
|
||||
PARANOIA_CB_VERIFY => {
|
||||
if self.stimeout >= 30 {
|
||||
if self.overlap as i64 > CD_FRAMEWORDS {
|
||||
self.slevel = 2;
|
||||
} else {
|
||||
self.slevel = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
PARANOIA_CB_READ => {
|
||||
if sector > self.c_sector {
|
||||
self.c_sector = sector;
|
||||
}
|
||||
}
|
||||
PARANOIA_CB_FIXUP_EDGE => {
|
||||
if self.stimeout >= 5 {
|
||||
if self.overlap as i64 > CD_FRAMEWORDS {
|
||||
self.slevel = 2;
|
||||
} else {
|
||||
self.slevel = 1;
|
||||
}
|
||||
}
|
||||
if self.dispcache[p] == b' ' {
|
||||
self.dispcache[p] = b'-';
|
||||
}
|
||||
}
|
||||
PARANOIA_CB_FIXUP_ATOM => {
|
||||
if self.slevel < 3 || self.stimeout > 5 {
|
||||
self.slevel = 3;
|
||||
}
|
||||
if self.dispcache[p] == b' ' || self.dispcache[p] == b'-' {
|
||||
self.dispcache[p] = b'+';
|
||||
}
|
||||
}
|
||||
PARANOIA_CB_READERR => {
|
||||
self.slevel = 6;
|
||||
if self.dispcache[p] != b'V' && self.dispcache[p] != b'C' {
|
||||
self.dispcache[p] = b'e';
|
||||
}
|
||||
}
|
||||
PARANOIA_CB_CACHEERR => {
|
||||
self.slevel = 8;
|
||||
self.dispcache[p] = b'C';
|
||||
}
|
||||
PARANOIA_CB_SKIP => {
|
||||
self.slevel = 8;
|
||||
if self.dispcache[p] != b'C' {
|
||||
self.dispcache[p] = b'V';
|
||||
}
|
||||
}
|
||||
PARANOIA_CB_OVERLAP => {
|
||||
self.overlap = osector as i32;
|
||||
}
|
||||
PARANOIA_CB_SCRATCH => {
|
||||
self.slevel = 7;
|
||||
}
|
||||
PARANOIA_CB_DRIFT => {
|
||||
if self.slevel < 4 || self.stimeout > 5 {
|
||||
self.slevel = 4;
|
||||
}
|
||||
}
|
||||
PARANOIA_CB_FIXUP_DROPPED | PARANOIA_CB_FIXUP_DUPED => {
|
||||
self.slevel = 5;
|
||||
if matches!(self.dispcache[p], b' ' | b'-' | b'+') {
|
||||
self.dispcache[p] = b'!';
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut smilie = "= :-)";
|
||||
match self.slevel {
|
||||
0 => {
|
||||
smilie = if self.skipped_flag { " 8-X" } else { " :^D" };
|
||||
}
|
||||
1 => smilie = " :-)",
|
||||
2 => smilie = " :-|",
|
||||
3 => smilie = " :-P",
|
||||
4 => smilie = " :-/",
|
||||
5 => smilie = " 8-|",
|
||||
6 => smilie = " :-0",
|
||||
7 => smilie = " :-(",
|
||||
8 => {
|
||||
smilie = " ;-(";
|
||||
self.skipped_flag = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let test = Self::now_tenths();
|
||||
if self.lasttime != test || function == -1 || self.slast != self.slevel {
|
||||
if self.lasttime != test || function == -1 {
|
||||
self.last += 1;
|
||||
self.lasttime = test;
|
||||
if self.last > 7 {
|
||||
self.last = 0;
|
||||
}
|
||||
self.stimeout += 1;
|
||||
self.heartbeat = match self.last {
|
||||
0 => b' ',
|
||||
1 | 7 => b'.',
|
||||
2 | 6 => b'o',
|
||||
3 | 5 => b'0',
|
||||
4 => b'O',
|
||||
_ => self.heartbeat,
|
||||
};
|
||||
if function == -1 {
|
||||
self.heartbeat = b'*';
|
||||
}
|
||||
}
|
||||
if self.slast != self.slevel {
|
||||
self.stimeout = 0;
|
||||
}
|
||||
self.slast = self.slevel;
|
||||
|
||||
let mut buffer: Vec<u8> =
|
||||
if self.abort_on_skip && self.skipped_flag && function != -1 {
|
||||
format!(
|
||||
"\r (== PROGRESS == [{}| {:06} {:02} ] =={} {} ==) ",
|
||||
" ...aborting; please wait... ",
|
||||
self.v_sector,
|
||||
self.overlap as i64 / CD_FRAMEWORDS,
|
||||
smilie,
|
||||
self.heartbeat as char
|
||||
)
|
||||
.into_bytes()
|
||||
} else {
|
||||
let disp = String::from_utf8_lossy(&self.dispcache).to_string();
|
||||
if self.v_sector == 0 {
|
||||
format!(
|
||||
"\r (== PROGRESS == [{}| ...... {:02} ] =={} {} ==) ",
|
||||
disp,
|
||||
self.overlap as i64 / CD_FRAMEWORDS,
|
||||
smilie,
|
||||
self.heartbeat as char
|
||||
)
|
||||
.into_bytes()
|
||||
} else {
|
||||
format!(
|
||||
"\r (== PROGRESS == [{}| {:06} {:02} ] =={} {} ==) ",
|
||||
disp,
|
||||
self.v_sector,
|
||||
self.overlap as i64 / CD_FRAMEWORDS,
|
||||
smilie,
|
||||
self.heartbeat as char
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
};
|
||||
|
||||
if aheadposition >= 0
|
||||
&& aheadposition < GRAPH as i32
|
||||
&& function != -1
|
||||
&& buffer.len() > 19 + aheadposition as usize
|
||||
{
|
||||
buffer[19 + aheadposition as usize] = b'>';
|
||||
}
|
||||
let _ = io::stderr().write_all(&buffer);
|
||||
|
||||
if function == -1 {
|
||||
let mut report_bytes = buffer[1..].to_vec();
|
||||
report_bytes.extend_from_slice(b"\n\n");
|
||||
log.write_summary_bytes(&report_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if function == -1 {
|
||||
self.dispcache = [b' '; GRAPH];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn skip_flag_survives_redirected_output() {
|
||||
let mut p = Progress::default();
|
||||
let mut log = Log::new();
|
||||
log.quiet = true;
|
||||
p.callback(0, PARANOIA_CB_SKIP, &mut log);
|
||||
assert!(p.skipped_flag);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
//! Span argument parsing, a direct port of `parse_offset()` from main.c.
|
||||
|
||||
use cdrustanoia_core::device::DiscToc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpanError(pub String);
|
||||
|
||||
impl std::fmt::Display for SpanError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for SpanError {}
|
||||
|
||||
fn err(msg: impl Into<String>) -> SpanError {
|
||||
SpanError(msg.into())
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
struct TimeParts {
|
||||
hours: Option<i64>,
|
||||
minutes: Option<i64>,
|
||||
seconds: Option<i64>,
|
||||
sectors: Option<i64>,
|
||||
}
|
||||
|
||||
fn parse_num(s: &str, original: &str) -> Result<i64, SpanError> {
|
||||
let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
if digits.is_empty() {
|
||||
Ok(0)
|
||||
} else {
|
||||
digits
|
||||
.parse::<i64>()
|
||||
.map_err(|_| err(format!("Error parsing span argument: {original}")))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_time(time: &str) -> Result<TimeParts, SpanError> {
|
||||
let mut parts = TimeParts::default();
|
||||
let mut rest = time;
|
||||
|
||||
if let Some(pos) = rest.rfind('.') {
|
||||
parts.sectors = Some(parse_num(&rest[pos + 1..], time)?);
|
||||
rest = &rest[..pos];
|
||||
if rest.contains('.') {
|
||||
return Err(err("Error parsing span argument"));
|
||||
}
|
||||
}
|
||||
|
||||
let fields: Vec<&str> = rest.split(':').collect();
|
||||
if fields.len() > 3 {
|
||||
return Err(err("Error parsing span argument"));
|
||||
}
|
||||
// The original walks right-to-left: rightmost field is seconds, then
|
||||
// minutes, then hours.
|
||||
match fields.len() {
|
||||
1 => {
|
||||
parts.seconds = Some(parse_num(fields[0], time)?);
|
||||
}
|
||||
2 => {
|
||||
parts.seconds = Some(parse_num(fields[1], time)?);
|
||||
parts.minutes = Some(parse_num(fields[0], time)?);
|
||||
}
|
||||
3 => {
|
||||
parts.seconds = Some(parse_num(fields[2], time)?);
|
||||
parts.minutes = Some(parse_num(fields[1], time)?);
|
||||
parts.hours = Some(parse_num(fields[0], time)?);
|
||||
}
|
||||
_ => return Err(err("Error parsing span argument")),
|
||||
}
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
fn has_time(parts: &TimeParts) -> bool {
|
||||
parts.seconds.is_some() || parts.sectors.is_some()
|
||||
}
|
||||
|
||||
/// Port of `parse_offset`. `begin` is the beginning of a span, if known.
|
||||
pub fn parse_offset(
|
||||
toc: &DiscToc,
|
||||
offset: Option<&str>,
|
||||
begin: Option<i64>,
|
||||
) -> Result<i64, SpanError> {
|
||||
let offset = match offset {
|
||||
Some(s) => s,
|
||||
None => return Ok(-1),
|
||||
};
|
||||
|
||||
let (track_text, time_text) = if let Some(close) = offset.find(']') {
|
||||
let before = &offset[..close];
|
||||
if let Some(open) = before.find('[') {
|
||||
(&before[..open], Some(&before[open + 1..]))
|
||||
} else {
|
||||
return Err(err("Error parsing span argument"));
|
||||
}
|
||||
} else {
|
||||
(offset, None)
|
||||
};
|
||||
|
||||
let digit_count = track_text
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_digit())
|
||||
.count();
|
||||
let track = if digit_count > 0 {
|
||||
let t = track_text[..digit_count]
|
||||
.parse::<i64>()
|
||||
.map_err(|_| err("Error parsing span argument"))?;
|
||||
if t < 0 || t > toc.tracks as i64 {
|
||||
return Err(err(format!("Track #{t} does not exist.")));
|
||||
}
|
||||
Some(t)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let parts = match time_text {
|
||||
Some(t) if !t.is_empty() => parse_time(t)?,
|
||||
Some(_) => TimeParts::default(),
|
||||
None => TimeParts::default(),
|
||||
};
|
||||
|
||||
let ret = match track {
|
||||
None => {
|
||||
if !has_time(&parts) {
|
||||
return Ok(-1);
|
||||
}
|
||||
begin.unwrap_or_else(|| toc.disc_firstsector())
|
||||
}
|
||||
Some(t) => {
|
||||
if !has_time(&parts) {
|
||||
if begin.is_none() {
|
||||
return Ok(toc.track_firstsector(t as i32));
|
||||
}
|
||||
return Ok(toc.track_lastsector(t as i32));
|
||||
}
|
||||
toc.track_firstsector(t as i32)
|
||||
}
|
||||
};
|
||||
|
||||
let mut ret = ret;
|
||||
let mut add = |v: i64, scale: i64| -> Result<(), SpanError> {
|
||||
ret = ret
|
||||
.checked_add(
|
||||
v.checked_mul(scale)
|
||||
.ok_or_else(|| err("Error parsing span argument"))?,
|
||||
)
|
||||
.ok_or_else(|| err("Error parsing span argument"))?;
|
||||
Ok(())
|
||||
};
|
||||
if let Some(v) = parts.sectors {
|
||||
add(v, 1)?;
|
||||
}
|
||||
if let Some(v) = parts.seconds {
|
||||
add(v, 75)?;
|
||||
}
|
||||
if let Some(v) = parts.minutes {
|
||||
add(v, 60 * 75)?;
|
||||
}
|
||||
if let Some(v) = parts.hours {
|
||||
add(v, 60 * 60 * 75)?;
|
||||
}
|
||||
|
||||
if let Some(t) = track {
|
||||
if toc.sector_gettrack(ret) != t as i32 {
|
||||
return Err(err(
|
||||
"Time/sector offset goes beyond end of specified track.",
|
||||
));
|
||||
}
|
||||
}
|
||||
if ret > toc.disc_lastsector() {
|
||||
return Err(err("Time/sector offset goes beyond end of disc."));
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Parse a complete span expression, returning inclusive first/last sectors.
|
||||
pub fn parse_span(toc: &DiscToc, span: &str) -> Result<(i64, i64), SpanError> {
|
||||
let first_hyphen = span.find('-');
|
||||
let last_hyphen = span.rfind('-');
|
||||
if first_hyphen != last_hyphen {
|
||||
return Err(err("Error parsing span argument"));
|
||||
}
|
||||
|
||||
let (part1, part2, had_hyphen) = match first_hyphen {
|
||||
Some(pos) => (&span[..pos], Some(&span[pos + 1..]), true),
|
||||
None => (span, None, false),
|
||||
};
|
||||
|
||||
let mut first = parse_offset(toc, Some(part1), None)?;
|
||||
let mut last = if first == -1 {
|
||||
parse_offset(toc, part2, Some(toc.disc_firstsector()))?
|
||||
} else {
|
||||
parse_offset(toc, part2, Some(first))?
|
||||
};
|
||||
|
||||
if first == -1 {
|
||||
if last == -1 {
|
||||
return Err(err("Error parsing span argument"));
|
||||
}
|
||||
first = toc.disc_firstsector();
|
||||
} else if last == -1 {
|
||||
if had_hyphen {
|
||||
last = toc.disc_lastsector();
|
||||
} else {
|
||||
let track = toc.sector_gettrack(first);
|
||||
last = toc.track_lastsector(track);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((first, last))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cdrustanoia_core::device::TocEntry;
|
||||
|
||||
fn toc() -> DiscToc {
|
||||
let mut t = DiscToc::default();
|
||||
t.entries[0] = TocEntry {
|
||||
flags: 0,
|
||||
track: 1,
|
||||
start_sector: 0,
|
||||
};
|
||||
t.entries[1] = TocEntry {
|
||||
flags: 0,
|
||||
track: 2,
|
||||
start_sector: 75000,
|
||||
};
|
||||
t.entries[2] = TocEntry {
|
||||
flags: 0,
|
||||
track: 3,
|
||||
start_sector: 150000,
|
||||
};
|
||||
t.entries[3] = TocEntry {
|
||||
flags: 0x04,
|
||||
track: 0xAA,
|
||||
start_sector: 225000,
|
||||
};
|
||||
t.tracks = 3;
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_track() {
|
||||
assert_eq!(parse_span(&toc(), "2").unwrap(), (75000, 149999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranges() {
|
||||
assert_eq!(parse_span(&toc(), "2-3").unwrap(), (75000, 224999));
|
||||
assert_eq!(parse_span(&toc(), "-2").unwrap(), (0, 149999));
|
||||
assert_eq!(parse_span(&toc(), "2-").unwrap(), (75000, 224999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn times() {
|
||||
assert_eq!(parse_span(&toc(), "1[1:00]").unwrap(), (4500, 74999));
|
||||
assert_eq!(parse_span(&toc(), "2[::20]").unwrap(), (76500, 149999));
|
||||
assert_eq!(parse_span(&toc(), "2[.30]").unwrap(), (75030, 149999));
|
||||
assert_eq!(parse_span(&toc(), "2[10:]").unwrap(), (120000, 149999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad() {
|
||||
assert!(parse_span(&toc(), "4").is_err());
|
||||
assert!(parse_span(&toc(), "1-2-3").is_err());
|
||||
assert!(parse_span(&toc(), "2[1:00:00:00]").is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use cdrustanoia_core::device::samples_to_bytes;
|
||||
|
||||
#[test]
|
||||
fn end_to_end_image_rip() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let image = dir.path().join("disc.raw");
|
||||
let out = dir.path().join("out.raw");
|
||||
let sectors = 24usize;
|
||||
let mut samples = Vec::with_capacity(sectors * 1176);
|
||||
for i in 0..sectors * 1176 {
|
||||
samples.push(((i as i32 * 7919) % 65536 - 32768) as i16);
|
||||
}
|
||||
std::fs::write(&image, samples_to_bytes(&samples)).unwrap();
|
||||
std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &image);
|
||||
|
||||
let argv = vec![
|
||||
"cdparanoia".to_string(),
|
||||
"-q".to_string(),
|
||||
"-r".to_string(),
|
||||
"1".to_string(),
|
||||
out.to_string_lossy().into_owned(),
|
||||
];
|
||||
assert_eq!(cdrustanoia_cli::run(&argv), 0);
|
||||
let got = std::fs::read(&out).unwrap();
|
||||
assert_eq!(got, samples_to_bytes(&samples));
|
||||
std::env::remove_var("CDRUSTANOIA_TEST_IMAGE");
|
||||
|
||||
// Multi-track batch mode.
|
||||
let multi = dir.path().join("multi.raw");
|
||||
let mut multi_samples = Vec::new();
|
||||
for i in 0..(48 * 1176) {
|
||||
multi_samples.push((((i as i64 * 104729) % 65536) as i32 - 32768) as i16);
|
||||
}
|
||||
std::fs::write(&multi, samples_to_bytes(&multi_samples)).unwrap();
|
||||
std::fs::write(dir.path().join("multi.raw.toc"), "0 23 0\n24 47 0\n").unwrap();
|
||||
std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &multi);
|
||||
let argv = vec![
|
||||
"cdparanoia".to_string(),
|
||||
"-q".to_string(),
|
||||
"-B".to_string(),
|
||||
"1-2".to_string(),
|
||||
format!("{}/", dir.path().display()),
|
||||
];
|
||||
assert_eq!(cdrustanoia_cli::run(&argv), 0);
|
||||
let t1 = std::fs::read(dir.path().join("track01.cdda.wav")).unwrap();
|
||||
let t2 = std::fs::read(dir.path().join("track02.cdda.wav")).unwrap();
|
||||
assert_eq!(&t1[44..], &samples_to_bytes(&multi_samples[..24 * 1176]));
|
||||
assert_eq!(&t2[44..], &samples_to_bytes(&multi_samples[24 * 1176..]));
|
||||
std::env::remove_var("CDRUSTANOIA_TEST_IMAGE");
|
||||
}
|
||||
Reference in New Issue
Block a user