1use std::path::{Path, PathBuf};
24
25use crate::disc::DvdDisc;
26use crate::error::{Error, Result};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum DvdUri {
31 AutoDetect,
33 Path(PathBuf),
35}
36
37pub 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#[derive(Debug)]
61pub struct DvdDiscSource {
62 pub disc: DvdDisc,
63 path: PathBuf,
64}
65
66impl DvdDiscSource {
67 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#[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 let source =
103 DvdDiscSource::open(&path).map_err(|e| CoreError::invalid(format!("dvd:// open: {e}")))?;
104 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#[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#[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}