use std::collections::BTreeSet;
use std::path::Path;
use crate::bdrom::disc::{BdRom, ScanMode, ScanObservers, ScanOptions, ScanReport};
use crate::error::BdError;
use crate::vfs::fs::FsDir;
use crate::vfs::udf::source::{PathIso, UdfSource};
use crate::vfs::volume;
pub fn open_folder(
path: &Path,
mode: ScanMode,
options: ScanOptions,
scan_files: Option<&BTreeSet<String>>,
observers: ScanObservers<'_>,
) -> Result<ScanReport, BdError> {
let probed = volume::drive_root_probe(path).and_then(volume::real_volume_label);
let root = FsDir::new(path);
let mut report = BdRom::open_resilient(&root, mode, options, scan_files, observers)?;
report.errors.extend(root.take_errors());
report.bdrom.volume_label = volume::apply_drive_root_probe(&report.bdrom.volume_label, probed);
Ok(report)
}
pub fn open_iso(
path: &Path,
mode: ScanMode,
options: ScanOptions,
scan_files: Option<&BTreeSet<String>>,
observers: ScanObservers<'_>,
) -> Result<ScanReport, BdError> {
let source = UdfSource::open_resilient(Box::new(PathIso::new(path)))?;
let mut report = BdRom::open_resilient(&source.root(), mode, options, scan_files, observers)?;
report.errors.extend(source.take_errors());
Ok(report)
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use super::{BdError, ScanMode, ScanObservers, ScanOptions, ScanReport, open_folder, open_iso};
use crate::bdrom::disc::ScanProgress;
use crate::bdrom::measured::MeasuredSnapshot;
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bdinfo-rs/tests/fixtures").join(name)
}
struct Scratch {
root: PathBuf,
}
impl Scratch {
fn new() -> Self {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir()
.join(format!("bdinfo-rs-scan-{}-{unique}", std::process::id()));
std::fs::create_dir_all(&root).expect("create the scratch dir");
Self { root }
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root).is_ok();
}
}
fn folder(path: &Path, mode: ScanMode) -> Result<ScanReport, BdError> {
let mut reported = 0_usize;
let cancel = AtomicBool::new(false);
let report = {
let mut count = |_: ScanProgress<'_>| reported = reported.saturating_add(1);
open_folder(
path,
mode,
ScanOptions::default(),
None,
ScanObservers::new(&mut count, &cancel),
)
};
assert_eq!(reported != 0, report.is_ok() && mode != ScanMode::Metadata);
report
}
fn iso(path: &Path, mode: ScanMode) -> Result<ScanReport, BdError> {
let mut reported = 0_usize;
let cancel = AtomicBool::new(false);
let report = {
let mut count = |_: ScanProgress<'_>| reported = reported.saturating_add(1);
open_iso(
path,
mode,
ScanOptions::default(),
None,
ScanObservers::new(&mut count, &cancel),
)
};
assert_eq!(reported != 0, report.is_ok() && mode != ScanMode::Metadata);
report
}
fn assert_the_live_tallies_land_on_the_summaries(
report: &ScanReport,
snapshots: &[MeasuredSnapshot],
) {
let playlist = report.bdrom.playlists.first().expect("the fixture lists one playlist");
let last = snapshots.last().expect("the measurement pass reported");
assert_eq!(last.file, "00000.M2TS", "the fixture's one stream file");
let measured = last.playlists.first().expect("the playlist plays that file");
assert_eq!(measured.name, playlist.name);
assert_eq!(measured.measured_bytes, playlist.total_angle_packet_size());
}
#[test]
fn an_observed_folder_open_streams_the_measured_tallies() {
let mut snapshots: Vec<MeasuredSnapshot> = Vec::new();
let cancel = AtomicBool::new(false);
let mut quiet = |_: ScanProgress<'_>| {};
let report = {
let mut watch = |snapshot| snapshots.push(snapshot);
open_folder(
&fixture("BigBuckBunny"),
ScanMode::Full,
ScanOptions::default(),
None,
ScanObservers::new(&mut quiet, &cancel).with_measured(&mut watch),
)
}
.expect("the fixture folder opens");
assert_eq!(report.bdrom.volume_label, "BigBuckBunny", "the label repair still runs");
assert_the_live_tallies_land_on_the_summaries(&report, &snapshots);
}
#[test]
fn an_observed_iso_open_streams_the_measured_tallies() {
let mut snapshots: Vec<MeasuredSnapshot> = Vec::new();
let cancel = AtomicBool::new(false);
let mut quiet = |_: ScanProgress<'_>| {};
let report = {
let mut watch = |snapshot| snapshots.push(snapshot);
open_iso(
&fixture("BigBuckBunny.iso"),
ScanMode::Full,
ScanOptions::default(),
None,
ScanObservers::new(&mut quiet, &cancel).with_measured(&mut watch),
)
}
.expect("the fixture image opens");
assert_eq!(report.bdrom.volume_label, "Blu-Ray", "the UDF volume label still reads");
assert_the_live_tallies_land_on_the_summaries(&report, &snapshots);
}
#[test]
fn a_folder_scan_names_the_disc_after_its_root_directory() {
let report =
folder(&fixture("BigBuckBunny"), ScanMode::Full).expect("the fixture folder opens");
assert_eq!(report.bdrom.volume_label, "BigBuckBunny");
assert!(!report.bdrom.playlists.is_empty(), "the structure lists");
assert!(report.errors.is_empty(), "healthy media records no failure");
}
#[test]
fn an_iso_scan_reads_the_real_udf_volume_label() {
let report =
iso(&fixture("BigBuckBunny.iso"), ScanMode::Full).expect("the fixture image opens");
assert_eq!(report.bdrom.volume_label, "Blu-Ray");
assert!(!report.bdrom.playlists.is_empty(), "the structure lists");
assert!(report.errors.is_empty(), "healthy media records no failure");
}
#[test]
fn a_healthy_disc_holds_no_stream_file_short_of_its_declared_span() {
let folder_scan =
folder(&fixture("BigBuckBunny"), ScanMode::Full).expect("the fixture folder opens");
assert!(folder_scan.bdrom.short_stream_files().is_empty());
let iso_scan =
iso(&fixture("BigBuckBunny.iso"), ScanMode::Full).expect("the fixture image opens");
assert!(iso_scan.bdrom.short_stream_files().is_empty());
}
#[test]
fn a_folder_without_a_bd_structure_fails() {
let scratch = Scratch::new();
let err = folder(&scratch.root, ScanMode::Metadata).expect_err("no BDMV to locate");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn a_file_that_is_no_udf_volume_fails() {
let scratch = Scratch::new();
let path = scratch.root.join("garbage.iso");
std::fs::write(&path, b"not a udf volume at all").expect("the scratch image writes");
let err = iso(&path, ScanMode::Metadata).expect_err("no UDF volume to open");
assert!(!err.to_string().is_empty(), "the failure carries a message");
}
#[test]
fn a_udf_volume_without_a_bd_structure_fails() {
let scratch = Scratch::new();
let path = scratch.root.join("renamed.iso");
let mut bytes = std::fs::read(fixture("BigBuckBunny.iso")).expect("the fixture reads");
let needle = b"BDMV";
let mut at = 0;
while let Some(hit) =
bytes.get(at..).and_then(|tail| tail.windows(needle.len()).position(|w| w == needle))
{
let start = at.checked_add(hit).expect("the offset fits");
let end = start.checked_add(needle.len()).expect("the offset fits");
bytes.get_mut(start..end).expect("the window is in range").copy_from_slice(b"XDMV");
at = start.checked_add(1).expect("the offset fits");
}
std::fs::write(&path, bytes).expect("the scratch image writes");
let err =
iso(&path, ScanMode::Metadata).expect_err("a volume with no BDMV cannot be scanned");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
}