Skip to main content

oxideav_dvd/
source.rs

1//! `dvd://` URI scheme — opens a DVD-Video ISO image or block device
2//! and surfaces it to `oxideav_core::SourceRegistry`.
3//!
4//! Supported URI forms:
5//!
6//! - `dvd:///abs/path/to/disc.iso` — open the file.
7//! - `dvd:///dev/sr0` — open a block device (Unix).
8//! - `dvd://` — Phase 2 (auto-detect a mounted DVD by walking
9//!   `/Volumes`, `/media`, `/mnt` and probing each candidate for
10//!   `VIDEO_TS/`). Currently returns `Unsupported`.
11//!
12//! Phase 1 surfaces the disc as a typed `DvdDiscSource`: a thin
13//! wrapper that carries the parsed [`DvdDisc`] enumeration plus the
14//! underlying file handle for byte-range reads. The reason we don't
15//! materialise the first VOB as a `BytesSource` (as the Blu-ray
16//! source does for the longest HDMV title) is that VOBs are MPEG-2
17//! Program Streams with DVD-specific nav-pack overlays: the
18//! pipeline needs to know it's a DVD before consuming bytes so it
19//! can route through a DVD-aware demuxer in Phase 2. For now the
20//! source driver makes the disc *discoverable* but the actual
21//! playback bridge is the Phase 2 deliverable.
22
23use std::path::{Path, PathBuf};
24
25use crate::disc::DvdDisc;
26use crate::error::{Error, Result};
27
28/// Parsed `dvd://` URI.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum DvdUri {
31    /// `dvd://` — auto-detect (Phase 2).
32    AutoDetect,
33    /// `dvd:///abs/path` — explicit file or block-device path.
34    Path(PathBuf),
35}
36
37/// Parse a `dvd://...` URI string.
38pub fn parse_dvd_uri(uri: &str) -> Result<DvdUri> {
39    let rest = uri
40        .strip_prefix("dvd://")
41        .or_else(|| uri.strip_prefix("dvd:"))
42        .ok_or(Error::NotDvdVideo("not a dvd:// URI"))?;
43    if rest.is_empty() || rest == "/" {
44        return Ok(DvdUri::AutoDetect);
45    }
46    let path = if let Some(p) = rest.strip_prefix('/') {
47        if p.starts_with('/') {
48            PathBuf::from(p)
49        } else {
50            PathBuf::from(format!("/{p}"))
51        }
52    } else {
53        PathBuf::from(rest)
54    };
55    Ok(DvdUri::Path(path))
56}
57
58/// Wrapper carrying the disc enumeration + the open file handle for
59/// byte-range reads. Phase 2 will add an `open_vob_reader` helper.
60#[derive(Debug)]
61pub struct DvdDiscSource {
62    pub disc: DvdDisc,
63    path: PathBuf,
64}
65
66impl DvdDiscSource {
67    /// Open a DVD-Video disc from a file or block-device path.
68    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
69        let path = path.as_ref().to_path_buf();
70        let disc = DvdDisc::open(&path)?;
71        Ok(Self { disc, path })
72    }
73
74    pub fn path(&self) -> &Path {
75        &self.path
76    }
77}
78
79/// `dvd://` source-registry entry point.
80#[cfg(feature = "registry")]
81pub fn open_dvd(uri: &str) -> oxideav_core::Result<Box<dyn oxideav_core::BytesSource>> {
82    use oxideav_core::Error as CoreError;
83    let parsed = parse_dvd_uri(uri).map_err(|e| CoreError::invalid(e.to_string()))?;
84    let path = match parsed {
85        DvdUri::AutoDetect => {
86            return Err(CoreError::invalid(
87                "dvd:// auto-detect is Phase 2 — pass an explicit dvd:///path/to/disc.iso",
88            ));
89        }
90        DvdUri::Path(p) => p,
91    };
92    if !path.exists() {
93        return Err(CoreError::invalid(format!(
94            "dvd:// path {} does not exist",
95            path.display()
96        )));
97    }
98    // Mount + enumerate so the caller sees a Phase-1-clean error if
99    // the disc is malformed. The returned `BytesSource` is the raw
100    // disc image bytes — Phase 2 will replace this with a proper
101    // VOB-stream source (clip concatenation + nav-pack stripping).
102    let source =
103        DvdDiscSource::open(&path).map_err(|e| CoreError::invalid(format!("dvd:// open: {e}")))?;
104    // Hand the disc image back as a plain byte stream so consumers
105    // that just want raw access (verifiers, hash sums) keep working.
106    // The Phase 2 demuxer will replace this with a typed wrapper.
107    let file = std::fs::File::open(source.path())
108        .map_err(|e| CoreError::invalid(format!("dvd:// reopen: {e}")))?;
109    Ok(Box::new(FileBytesSource { file }))
110}
111
112/// Tiny `BytesSource` adapter around an `std::fs::File`. Mirrors the
113/// shape `oxideav_core::BytesSource` expects (just `Read + Seek`).
114#[cfg(feature = "registry")]
115struct FileBytesSource {
116    file: std::fs::File,
117}
118
119#[cfg(feature = "registry")]
120impl std::io::Read for FileBytesSource {
121    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
122        self.file.read(buf)
123    }
124}
125
126#[cfg(feature = "registry")]
127impl std::io::Seek for FileBytesSource {
128    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
129        self.file.seek(pos)
130    }
131}
132
133#[cfg(feature = "registry")]
134impl std::fmt::Debug for FileBytesSource {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("FileBytesSource").finish()
137    }
138}
139
140// `oxideav_core::BytesSource` has a blanket impl for `T: Read + Seek + Send`,
141// so `FileBytesSource` (wrapping `std::fs::File`) picks it up automatically.
142
143/// Register the `dvd` scheme with a [`oxideav_core::RuntimeContext`].
144#[cfg(feature = "registry")]
145pub fn register(ctx: &mut oxideav_core::RuntimeContext) {
146    ctx.sources.register_bytes("dvd", open_dvd);
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn parse_auto_detect() {
155        assert_eq!(parse_dvd_uri("dvd://").unwrap(), DvdUri::AutoDetect);
156        assert_eq!(parse_dvd_uri("dvd:").unwrap(), DvdUri::AutoDetect);
157        assert_eq!(parse_dvd_uri("dvd:///").unwrap(), DvdUri::AutoDetect);
158    }
159
160    #[test]
161    fn parse_absolute_path() {
162        assert_eq!(
163            parse_dvd_uri("dvd:///tmp/disc.iso").unwrap(),
164            DvdUri::Path(PathBuf::from("/tmp/disc.iso"))
165        );
166        assert_eq!(
167            parse_dvd_uri("dvd:///dev/sr0").unwrap(),
168            DvdUri::Path(PathBuf::from("/dev/sr0"))
169        );
170    }
171
172    #[test]
173    fn rejects_wrong_scheme() {
174        assert!(parse_dvd_uri("file:///x").is_err());
175        assert!(parse_dvd_uri("http://example/").is_err());
176    }
177}