#[cfg(any(windows, test))]
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
use crate::discovery::BdmvDir;
#[cfg(any(windows, test))]
use crate::vfs::ReadSeek;
#[cfg(windows)]
use crate::vfs::udf::source::{IsoReader, UdfSource};
fn drive_root_letter(label: &str) -> Option<char> {
let core = label.strip_suffix(['/', '\\']).unwrap_or(label);
let mut chars = core.chars();
let letter = chars.next()?;
(letter.is_ascii_alphabetic() && chars.next() == Some(':') && chars.next().is_none())
.then(|| letter.to_ascii_uppercase())
}
#[must_use]
pub fn resolve_folder_label(scanned: &str) -> String {
resolve_with(scanned, real_volume_label)
}
pub(crate) fn drive_root_probe(path: &Path) -> Option<char> {
let text = path.to_string_lossy();
let mut chars = text.chars();
let letter = chars.next()?;
if !letter.is_ascii_alphabetic() || chars.next() != Some(':') {
return None;
}
chars
.as_str()
.split(['/', '\\'])
.all(|part| part.is_empty() || BdmvDir::from_name(part).is_some())
.then(|| letter.to_ascii_uppercase())
}
#[must_use]
pub(crate) fn apply_drive_root_probe(scanned: &str, probed: Option<String>) -> String {
resolve_with(scanned, move |_| probed)
}
fn resolve_with(scanned: &str, read: impl FnOnce(char) -> Option<String>) -> String {
drive_root_letter(scanned).map_or_else(
|| scanned.to_owned(),
|letter| read(letter).unwrap_or_else(|| letter.to_string()),
)
}
#[cfg(windows)]
const RAW_READ_ATTEMPTS: usize = 3;
#[cfg(windows)]
const RAW_READ_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(150);
#[cfg(windows)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) fn real_volume_label(letter: char) -> Option<String> {
let iso = RawVolumeIso { device: format!(r"\\.\{letter}:") };
for attempt in 0..RAW_READ_ATTEMPTS {
if attempt != 0 {
std::thread::sleep(RAW_READ_RETRY_DELAY);
}
if let Ok(label) = UdfSource::read_label(&iso) {
return (!label.is_empty()).then_some(label);
}
}
None
}
#[cfg(not(windows))]
#[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) const fn real_volume_label(_letter: char) -> Option<String> {
None
}
#[cfg(any(windows, test))]
const SECTOR: u64 = 2048;
#[cfg(any(windows, test))]
const SECTOR_USIZE: usize = 2048;
#[cfg(windows)]
#[derive(Debug)]
struct RawVolumeIso {
device: String,
}
#[cfg(windows)]
impl IsoReader for RawVolumeIso {
#[cfg_attr(coverage_nightly, coverage(off))]
fn open(&self) -> io::Result<Box<dyn ReadSeek>> {
Ok(Box::new(AlignedReader::new(Box::new(std::fs::File::open(&self.device)?))))
}
}
#[cfg(any(windows, test))]
struct AlignedReader {
inner: Box<dyn ReadSeek>,
pos: u64,
buf: Vec<u8>,
valid: usize,
cached: Option<u64>,
}
#[cfg(any(windows, test))]
impl AlignedReader {
fn new(inner: Box<dyn ReadSeek>) -> Self {
Self { inner, pos: 0, buf: vec![0; SECTOR_USIZE], valid: 0, cached: None }
}
fn fill(&mut self, sector: u64) -> io::Result<()> {
if self.cached == Some(sector) {
return Ok(());
}
let offset = sector.wrapping_mul(SECTOR);
self.inner.seek(SeekFrom::Start(offset))?;
self.valid = self.inner.read(self.buf.as_mut_slice())?;
self.cached = Some(sector);
Ok(())
}
}
#[cfg(any(windows, test))]
impl Read for AlignedReader {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
if out.is_empty() {
return Ok(0);
}
let sector = self.pos.wrapping_div(SECTOR);
let within = self.pos.wrapping_rem(SECTOR);
self.fill(sector)?;
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "within < SECTOR (2048) is an exact usize"
)]
let within = within as usize;
let Some(src) = self.buf.get(within..self.valid) else {
return Ok(0);
};
let n = src.len().min(out.len());
for (dst, &byte) in out.iter_mut().zip(src) {
*dst = byte;
}
#[expect(clippy::as_conversions, reason = "n <= SECTOR (2048) is an exact u64")]
let advance = n as u64;
self.pos = self.pos.wrapping_add(advance);
Ok(n)
}
}
#[cfg(any(windows, test))]
impl Seek for AlignedReader {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
let target = match from {
SeekFrom::Start(n) => n,
SeekFrom::Current(delta) => add_signed(self.pos, delta)?,
SeekFrom::End(delta) => add_signed(self.inner.seek(SeekFrom::End(0))?, delta)?,
};
self.pos = target;
Ok(target)
}
}
#[cfg(any(windows, test))]
fn add_signed(base: u64, delta: i64) -> io::Result<u64> {
let target = if delta < 0 {
base.checked_sub(delta.unsigned_abs())
} else {
base.checked_add(delta.unsigned_abs())
};
target.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek out of range"))
}
#[cfg(test)]
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
clippy::indexing_slicing,
reason = "controlled, in-range sector casts and slicing in test fixtures"
)]
mod tests {
use std::io::{Cursor, Read as _, Seek as _, SeekFrom};
use std::path::Path;
use super::{
AlignedReader, SECTOR, add_signed, apply_drive_root_probe, drive_root_letter,
drive_root_probe, resolve_folder_label, resolve_with,
};
#[test]
fn drive_root_letter_matches_every_bare_drive_root_spelling() {
assert_eq!(drive_root_letter(r"J:\"), Some('J'));
assert_eq!(drive_root_letter("k:/"), Some('K'));
assert_eq!(drive_root_letter("k:"), Some('K'));
assert_eq!(drive_root_letter("Z:/"), Some('Z'));
}
#[test]
fn drive_root_letter_rejects_real_names() {
assert_eq!(drive_root_letter("BigBuckBunny"), None);
assert_eq!(drive_root_letter(""), None);
assert_eq!(drive_root_letter("C:temp"), None); assert_eq!(drive_root_letter("1:/"), None); assert_eq!(drive_root_letter(r"J:\sub"), None);
}
#[test]
fn resolve_with_consults_the_reader_only_for_a_drive_root() {
let read = |letter: char| Some(format!("VOL{letter}"));
assert_eq!(resolve_with(r"J:\", read), "VOLJ");
assert_eq!(resolve_with("BigBuckBunny", read), "BigBuckBunny");
}
#[test]
fn resolve_with_falls_back_to_the_drive_letter_when_the_read_fails() {
assert_eq!(resolve_with(r"J:\", |_| None), "J");
assert_eq!(resolve_with("k:/", |_| None), "K");
}
#[test]
fn resolve_folder_label_leaves_a_real_name_untouched() {
assert_eq!(resolve_folder_label("BigBuckBunny"), "BigBuckBunny");
}
#[test]
fn a_probe_is_owed_by_every_drive_root_input_spelling() {
assert_eq!(drive_root_probe(Path::new(r"J:\")), Some('J'));
assert_eq!(drive_root_probe(Path::new("k:/")), Some('K'));
assert_eq!(drive_root_probe(Path::new("k:")), Some('K'));
assert_eq!(drive_root_probe(Path::new(r"J:\BDMV")), Some('J'));
assert_eq!(drive_root_probe(Path::new("j:/bdmv/stream/")), Some('J'));
assert_eq!(drive_root_probe(Path::new(r"J:\BDMV\PLAYLIST")), Some('J'));
}
#[test]
fn no_probe_is_owed_where_the_scan_will_find_a_name() {
assert_eq!(drive_root_probe(Path::new(r"J:\Rips\Disc\BDMV")), None);
assert_eq!(drive_root_probe(Path::new(r"J:\BigBuckBunny")), None);
assert_eq!(drive_root_probe(Path::new("BigBuckBunny")), None);
assert_eq!(drive_root_probe(Path::new("/media/user/MY_DISC/BDMV")), None);
assert_eq!(drive_root_probe(Path::new("")), None);
assert_eq!(drive_root_probe(Path::new("1:/")), None); assert_eq!(drive_root_probe(Path::new("Jx")), None); }
#[test]
fn a_probed_label_reaches_a_drive_root_scan_whatever_the_scan_recorded() {
assert_eq!(apply_drive_root_probe(r"J:\", Some("MY_DISC".to_owned())), "MY_DISC");
assert_eq!(apply_drive_root_probe("k:/", Some("MY_DISC".to_owned())), "MY_DISC");
assert_eq!(apply_drive_root_probe(r"J:\", None), "J");
assert_eq!(
apply_drive_root_probe("BigBuckBunny", Some("MY_DISC".to_owned())),
"BigBuckBunny"
);
assert_eq!(apply_drive_root_probe("BigBuckBunny", None), "BigBuckBunny");
}
struct AlignmentGuard {
cursor: Cursor<Vec<u8>>,
}
impl std::io::Read for AlignmentGuard {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let pos = self.cursor.position();
let len = buf.len() as u64;
assert_eq!(pos % SECTOR, 0, "unaligned read offset {pos}");
assert_eq!(len % SECTOR, 0, "unaligned read length {len}");
self.cursor.read(buf)
}
}
impl std::io::Seek for AlignmentGuard {
fn seek(&mut self, from: SeekFrom) -> std::io::Result<u64> {
self.cursor.seek(from)
}
}
fn ramp(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
fn aligned(bytes: Vec<u8>) -> AlignedReader {
AlignedReader::new(Box::new(AlignmentGuard { cursor: Cursor::new(bytes) }))
}
#[test]
fn reads_a_sub_sector_range_only_issuing_aligned_device_reads() {
let data = ramp(3 * SECTOR as usize);
let mut reader = aligned(data.clone());
reader.seek(SeekFrom::Start(100)).expect("seek");
let mut got = [0_u8; 50];
reader.read_exact(&mut got).expect("read");
assert_eq!(got.as_slice(), &data[100..150]);
}
#[test]
fn reads_spanning_a_sector_boundary_reassemble_correctly() {
let data = ramp(3 * SECTOR as usize);
let mut reader = aligned(data.clone());
let start = SECTOR as usize - 20;
reader.seek(SeekFrom::Start(start as u64)).expect("seek");
let mut got = vec![0_u8; 100];
reader.read_exact(&mut got).expect("read");
assert_eq!(got, data[start..start + 100]);
}
#[test]
fn a_cached_sector_is_reused_across_reads() {
let data = ramp(2 * SECTOR as usize);
let mut reader = aligned(data.clone());
let mut a = [0_u8; 10];
let mut b = [0_u8; 10];
reader.seek(SeekFrom::Start(0)).expect("seek");
reader.read_exact(&mut a).expect("read a");
reader.read_exact(&mut b).expect("read b"); assert_eq!(a, data[0..10]);
assert_eq!(b, data[10..20]);
}
#[test]
fn reading_at_or_past_the_end_yields_zero() {
let data = ramp(SECTOR as usize + 100); let mut reader = aligned(data.clone());
reader.seek(SeekFrom::Start(data.len() as u64)).expect("seek to end");
let mut got = [0_u8; 16];
assert_eq!(reader.read(&mut got).expect("read at end"), 0);
reader.seek(SeekFrom::Start(SECTOR + 150)).expect("seek past tail");
assert_eq!(reader.read(&mut got).expect("read past tail"), 0);
}
#[test]
fn an_empty_output_buffer_reads_nothing() {
let mut reader = aligned(ramp(SECTOR as usize));
assert_eq!(reader.read(&mut []).expect("empty read"), 0);
}
#[test]
fn seek_current_and_end_land_on_the_right_bytes() {
let data = ramp(3 * SECTOR as usize);
let mut reader = aligned(data.clone());
reader.seek(SeekFrom::Start(200)).expect("seek start");
reader.seek(SeekFrom::Current(50)).expect("seek current +");
let mut got = [0_u8; 4];
reader.read_exact(&mut got).expect("read");
assert_eq!(got.as_slice(), &data[250..254]);
let end = reader.seek(SeekFrom::End(-10)).expect("seek end -10");
assert_eq!(end, data.len() as u64 - 10);
reader.seek(SeekFrom::Current(-4)).expect("seek current -");
let mut tail = [0_u8; 4];
reader.read_exact(&mut tail).expect("read tail");
assert_eq!(tail.as_slice(), &data[data.len() - 14..data.len() - 10]);
}
#[test]
fn add_signed_handles_both_directions_and_reports_out_of_range() {
assert_eq!(add_signed(100, 20).expect("add"), 120);
assert_eq!(add_signed(100, -20).expect("sub"), 80);
assert!(add_signed(10, -20).is_err()); assert!(add_signed(u64::MAX, 1).is_err()); }
#[test]
fn a_seek_before_the_start_errors() {
let mut reader = aligned(ramp(SECTOR as usize));
assert!(reader.seek(SeekFrom::Current(-1)).is_err());
}
#[test]
fn an_end_relative_seek_before_the_start_errors() {
let mut reader = aligned(ramp(2 * SECTOR as usize));
assert!(reader.seek(SeekFrom::End(i64::MIN)).is_err());
}
struct FailingInner {
fail_seek: bool,
fail_read: bool,
}
impl std::io::Read for FailingInner {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
if self.fail_read { Err(std::io::Error::other("read boom")) } else { Ok(0) }
}
}
impl std::io::Seek for FailingInner {
fn seek(&mut self, _from: SeekFrom) -> std::io::Result<u64> {
if self.fail_seek { Err(std::io::Error::other("seek boom")) } else { Ok(0) }
}
}
fn failing(fail_seek: bool, fail_read: bool) -> AlignedReader {
AlignedReader::new(Box::new(FailingInner { fail_seek, fail_read }))
}
#[test]
fn a_failing_inner_seek_propagates_through_read() {
assert!(failing(true, false).read(&mut [0_u8; 4]).is_err());
}
#[test]
fn a_failing_inner_read_propagates_through_read() {
assert!(failing(false, true).read(&mut [0_u8; 4]).is_err());
}
#[test]
fn a_failing_inner_end_seek_propagates() {
assert!(failing(true, false).seek(SeekFrom::End(0)).is_err());
}
#[test]
fn a_non_failing_inner_serves_an_empty_device() {
assert_eq!(failing(false, false).read(&mut [0_u8; 4]).expect("read"), 0);
}
mod prop {
use proptest::prelude::{any, prop_assert_eq, proptest};
use super::super::{drive_root_letter, resolve_with};
proptest! {
#[test]
fn resolution_is_identity_off_the_drive_root(label in any::<String>()) {
let resolved = resolve_with(&label, |_| None);
if let Some(letter) = drive_root_letter(&label) {
prop_assert_eq!(resolved, letter.to_string());
} else {
prop_assert_eq!(resolved, label);
}
}
}
}
}