first draft

This commit is contained in:
tux
2026-11-03 20:19:21 +01:00
commit 158e115242
44 changed files with 10785 additions and 0 deletions
+25
View File
@@ -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"
+295
View File
@@ -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
}
+420
View File
@@ -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());
}
}
+4
View File
@@ -0,0 +1,4 @@
fn main() {
let argv: Vec<String> = std::env::args().collect();
std::process::exit(cdrustanoia_cli::run(&argv));
}
+806
View File
@@ -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(&sectors) {
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
}
+121
View File
@@ -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)*))
};
}
+4
View File
@@ -0,0 +1,4 @@
fn main() {
let argv: Vec<String> = std::env::args().collect();
std::process::exit(cdrustanoia_cli::run(&argv));
}
+226
View File
@@ -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");
}
}
+351
View File
@@ -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);
}
}
+271
View File
@@ -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());
}
}
+50
View File
@@ -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");
}