Skip to main content

libfreemkv/mux/
resolve.rs

1//! Stream URL resolver — parses URL strings into PES stream instances.
2//!
3//! Format: `scheme://path`
4//!
5//! | Scheme | Input | Output | Path |
6//! |--------|-------|--------|------|
7//! | disc:// | Yes | -- | empty (auto-detect) or /dev/sgN |
8//! | iso://  | Yes | -- | file path (required) |
9//! | mkv://  | Yes | Yes | file path (required) |
10//! | m2ts:// | Yes | Yes | file path (required) |
11//! | network:// | Yes (listen) | Yes (connect) | host:port (required) |
12//! | stdio:// | Yes (stdin) | Yes (stdout) | empty |
13//! | null:// | -- | Yes | empty |
14//!
15//! Bare paths without a scheme are rejected.
16//! For disc→ISO (raw sector copy), use `Disc::copy()` instead.
17
18use super::disc::DiscStream;
19use super::network::NetworkStream;
20use super::null::NullStream;
21use super::stdio::StdioStream;
22use super::{M2tsStream, MkvStream};
23use std::io;
24use std::path::{Path, PathBuf};
25
26/// I/O buffer size for file streams.
27const IO_BUF_SIZE: usize = 4 * 1024 * 1024;
28
29/// Parsed stream URL.
30pub enum StreamUrl {
31    /// Optical disc drive. Device path is optional (auto-detect if None).
32    Disc { device: Option<PathBuf> },
33    /// MPEG-2 transport stream file.
34    M2ts { path: PathBuf },
35    /// Matroska container file.
36    Mkv { path: PathBuf },
37    /// Network stream (host:port).
38    Network { addr: String },
39    /// Standard I/O (stdin/stdout).
40    Stdio,
41    /// ISO disc image file.
42    Iso { path: PathBuf },
43    /// Null sink (write-only, discards data).
44    Null,
45    /// Unrecognized URL.
46    Unknown { raw: String },
47}
48
49impl StreamUrl {
50    /// The scheme name (e.g. "disc", "mkv", "null").
51    pub fn scheme(&self) -> &str {
52        match self {
53            StreamUrl::Disc { .. } => "disc",
54            StreamUrl::M2ts { .. } => "m2ts",
55            StreamUrl::Mkv { .. } => "mkv",
56            StreamUrl::Network { .. } => "network",
57            StreamUrl::Stdio => "stdio",
58            StreamUrl::Iso { .. } => "iso",
59            StreamUrl::Null => "null",
60            StreamUrl::Unknown { .. } => "unknown",
61        }
62    }
63
64    /// The path/address component, or empty string for scheme-only URLs.
65    pub fn path_str(&self) -> &str {
66        match self {
67            StreamUrl::Disc { device: Some(p) } => p.to_str().unwrap_or(""),
68            StreamUrl::Disc { device: None } => "",
69            StreamUrl::M2ts { path } | StreamUrl::Mkv { path } | StreamUrl::Iso { path } => {
70                path.to_str().unwrap_or("")
71            }
72            StreamUrl::Network { addr } => addr,
73            StreamUrl::Stdio | StreamUrl::Null => "",
74            StreamUrl::Unknown { raw } => raw,
75        }
76    }
77
78    /// Whether this URL represents a disc source (disc:// or iso://).
79    pub fn is_disc_source(&self) -> bool {
80        matches!(self, StreamUrl::Disc { .. } | StreamUrl::Iso { .. })
81    }
82}
83
84/// Parse a URL string into a typed StreamUrl.
85pub fn parse_url(url: &str) -> StreamUrl {
86    if let Some(rest) = url.strip_prefix("disc://") {
87        return if rest.is_empty() {
88            StreamUrl::Disc { device: None }
89        } else {
90            StreamUrl::Disc {
91                device: Some(PathBuf::from(rest)),
92            }
93        };
94    }
95    if let Some(rest) = url.strip_prefix("m2ts://") {
96        return StreamUrl::M2ts {
97            path: PathBuf::from(rest),
98        };
99    }
100    if let Some(rest) = url.strip_prefix("mkv://") {
101        return StreamUrl::Mkv {
102            path: PathBuf::from(rest),
103        };
104    }
105    if let Some(rest) = url.strip_prefix("network://") {
106        return StreamUrl::Network {
107            addr: rest.to_string(),
108        };
109    }
110    if url == "null://" || url.starts_with("null://") {
111        return StreamUrl::Null;
112    }
113    if url == "stdio://" || url.starts_with("stdio://") {
114        return StreamUrl::Stdio;
115    }
116    if let Some(rest) = url.strip_prefix("iso://") {
117        return StreamUrl::Iso {
118            path: PathBuf::from(rest),
119        };
120    }
121    StreamUrl::Unknown {
122        raw: url.to_string(),
123    }
124}
125
126/// Validate that a file path is non-empty and has a filename component.
127fn validate_file_path(path: &Path, scheme: &str) -> io::Result<()> {
128    if path.as_os_str().is_empty() {
129        return Err(crate::error::Error::StreamUrlMissingPath {
130            scheme: scheme.to_string(),
131        }
132        .into());
133    }
134    if path.file_name().is_none() {
135        return Err(crate::error::Error::StreamUrlInvalid {
136            url: format!("{scheme}://{}", path.display()),
137        }
138        .into());
139    }
140    Ok(())
141}
142
143/// Validate that a network address has host:port format.
144fn validate_network_addr(addr: &str) -> io::Result<()> {
145    if addr.is_empty() {
146        return Err(crate::error::Error::StreamUrlMissingPath {
147            scheme: "network".to_string(),
148        }
149        .into());
150    }
151    if !addr.contains(':') {
152        return Err(crate::error::Error::StreamUrlMissingPort {
153            addr: addr.to_string(),
154        }
155        .into());
156    }
157    Ok(())
158}
159
160/// Options for opening an input stream.
161#[derive(Default)]
162pub struct InputOptions {
163    pub keydb_path: Option<String>,
164    pub title_index: Option<usize>,
165    /// Skip decryption — return raw encrypted bytes.
166    pub raw: bool,
167}
168
169/// Open a PES input stream (produces PES frames).
170pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::Stream>> {
171    let parsed = parse_url(url);
172    match parsed {
173        StreamUrl::Disc { device } => {
174            // Open drive, init, scan — caller manages the drive
175            let mut drive = match device {
176                Some(ref d) => {
177                    crate::drive::Drive::open(d).map_err(|e| -> io::Error { e.into() })?
178                }
179                None => crate::drive::find_drive().ok_or_else(|| -> io::Error {
180                    crate::error::Error::DeviceNotFound {
181                        path: String::new(),
182                    }
183                    .into()
184                })?,
185            };
186            let _ = drive.wait_ready();
187            let _ = drive.init();
188            let _ = drive.probe_disc();
189            let (mut stream, _disc) = DiscStream::open_drive(
190                drive,
191                opts.keydb_path.as_deref(),
192                opts.title_index.unwrap_or(0),
193            )
194            .map_err(|e| -> io::Error { e.into() })?;
195            if opts.raw {
196                stream.set_raw();
197            }
198            Ok(Box::new(stream))
199        }
200        StreamUrl::Iso { ref path } => {
201            validate_file_path(path, "iso")?;
202            let scan_opts = match &opts.keydb_path {
203                Some(p) => crate::disc::ScanOptions::with_keydb(p),
204                None => crate::disc::ScanOptions::default(),
205            };
206            let mut stream =
207                DiscStream::open_iso(&path.to_string_lossy(), opts.title_index, &scan_opts)?;
208            if opts.raw {
209                stream.set_raw();
210            }
211            Ok(Box::new(stream))
212        }
213        StreamUrl::M2ts { ref path } => {
214            validate_file_path(path, "m2ts")?;
215            let file = std::fs::File::open(path).map_err(|e| {
216                io::Error::new(e.kind(), format!("m2ts://{}: {}", path.display(), e))
217            })?;
218            let reader = std::io::BufReader::with_capacity(IO_BUF_SIZE, file);
219            Ok(Box::new(M2tsStream::open(reader)?))
220        }
221        StreamUrl::Mkv { ref path } => {
222            validate_file_path(path, "mkv")?;
223            let file = std::fs::File::open(path).map_err(|e| {
224                io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e))
225            })?;
226            let reader = std::io::BufReader::with_capacity(IO_BUF_SIZE, file);
227            Ok(Box::new(MkvStream::open(reader)?))
228        }
229        StreamUrl::Network { ref addr } => {
230            validate_network_addr(addr)?;
231            Ok(Box::new(NetworkStream::listen(addr)?))
232        }
233        StreamUrl::Stdio => Ok(Box::new(StdioStream::input())),
234        StreamUrl::Null => Err(crate::error::Error::StreamWriteOnly.into()),
235        StreamUrl::Unknown { ref raw } => {
236            Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
237        }
238    }
239}
240
241/// Open a PES output stream (consumes PES frames).
242pub fn output(
243    url: &str,
244    title: &crate::disc::DiscTitle,
245) -> io::Result<Box<dyn crate::pes::Stream>> {
246    let parsed = parse_url(url);
247    match parsed {
248        StreamUrl::Mkv { ref path } => {
249            validate_file_path(path, "mkv")?;
250            let file = std::fs::File::create(path).map_err(|e| {
251                io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e))
252            })?;
253            let writer: Box<dyn super::WriteSeek> =
254                Box::new(std::io::BufWriter::with_capacity(IO_BUF_SIZE, file));
255            Ok(Box::new(MkvStream::create(writer, title)?))
256        }
257        StreamUrl::M2ts { ref path } => {
258            validate_file_path(path, "m2ts")?;
259            let file = std::fs::File::create(path).map_err(|e| {
260                io::Error::new(e.kind(), format!("m2ts://{}: {}", path.display(), e))
261            })?;
262            let writer = std::io::BufWriter::with_capacity(IO_BUF_SIZE, file);
263            Ok(Box::new(M2tsStream::create(writer, title)?))
264        }
265        StreamUrl::Network { ref addr } => {
266            validate_network_addr(addr)?;
267            Ok(Box::new(NetworkStream::connect(addr)?.meta(title)))
268        }
269        StreamUrl::Stdio => Ok(Box::new(StdioStream::output(title))),
270        StreamUrl::Null => Ok(Box::new(NullStream::new(title))),
271        StreamUrl::Disc { .. } => Err(crate::error::Error::StreamReadOnly.into()),
272        StreamUrl::Iso { .. } => Err(crate::error::Error::StreamReadOnly.into()),
273        StreamUrl::Unknown { ref raw } => {
274            Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
275        }
276    }
277}