Skip to main content

miden_debug/
input.rs

1use alloc::{borrow::Cow, boxed::Box};
2
3use miden_debug_types::Uri;
4
5#[derive(Clone)]
6pub struct InputFile {
7    path: Uri,
8    content: Option<Box<[u8]>>,
9}
10
11impl core::fmt::Debug for InputFile {
12    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
13        use alloc::string::ToString;
14
15        let content = match self.content.as_deref() {
16            None => "None".to_string(),
17            Some(content) => {
18                format!("Some({{ length: {}, data: .. }})", content.len())
19            }
20        };
21        f.debug_struct("InputFile")
22            .field("path", &self.path)
23            .field("content", &content)
24            .finish()
25    }
26}
27
28impl Default for InputFile {
29    fn default() -> Self {
30        Self {
31            path: Uri::new("stdin://"),
32            content: Some(Box::from([])),
33        }
34    }
35}
36
37#[derive(Debug, thiserror::Error)]
38#[non_exhaustive]
39pub enum InvalidInputError {
40    #[error("invalid input: unsupported uri scheme in '{0}'")]
41    UnsupportedScheme(Uri),
42    #[error("expected valid file path, got '{0}'")]
43    InvalidPath(Uri),
44    #[cfg(feature = "std")]
45    #[error("failed to read input file: {0}")]
46    Io(#[from] std::io::Error),
47}
48
49impl InputFile {
50    pub fn uri(&self) -> &Uri {
51        &self.path
52    }
53
54    pub fn file_name(&self) -> &str {
55        match self.path.scheme().unwrap_or("file") {
56            "stdin" => match self.path.as_str().rsplit_once('/') {
57                None => self.path.as_str().strip_prefix("stdin://").unwrap(),
58                Some((_, "")) => "<noname>",
59                Some((_, file_name)) => file_name,
60            },
61            _ => match self.path.as_str().rsplit_once('/') {
62                None => self.path.as_str().split_once("://").unwrap().1,
63                Some((_, file_name)) => file_name,
64            },
65        }
66    }
67
68    #[cfg(feature = "std")]
69    pub fn bytes(&self) -> Result<Cow<'_, [u8]>, InvalidInputError> {
70        match self.path.scheme() {
71            Some("stdin") => Ok(Cow::Borrowed(self.content.as_deref().unwrap_or(&[]))),
72            Some("file") | None => {
73                let path = self
74                    .path
75                    .to_path()
76                    .ok_or_else(|| InvalidInputError::InvalidPath(self.path.clone()))?;
77                std::fs::read(path).map(Cow::Owned).map_err(InvalidInputError::Io)
78            }
79            Some(_) => Err(InvalidInputError::UnsupportedScheme(self.path.clone())),
80        }
81    }
82
83    #[cfg(not(feature = "std"))]
84    pub fn bytes(&self) -> Result<Cow<'_, [u8]>, InvalidInputError> {
85        Ok(Cow::Borrowed(self.content.as_deref().unwrap_or(&[])))
86    }
87
88    /// Create a new [InputFile] from a raw [Uri] and the content associated with it, if any
89    ///
90    /// If no content is provided, then the URI must be loadable from disk, which requires the `std`
91    /// feature. If you are not building with the `std` feature enabled, then you should provide
92    /// the content here, or the input file will be useless.
93    pub fn new(path: impl Into<Uri>, content: Option<Box<[u8]>>) -> Self {
94        Self {
95            path: path.into(),
96            content,
97        }
98    }
99
100    /// Get an [InputFile] representing the contents of `path`.
101    ///
102    /// This function returns an error if the contents are not a valid supported file type.
103    #[cfg(feature = "std")]
104    pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> Self {
105        let path = path.as_ref();
106        Self {
107            path: Uri::from(path),
108            content: None,
109        }
110    }
111
112    /// Get an [InputFile] representing the contents received from standard input.
113    ///
114    /// This function returns an error if the contents are not a valid supported file type.
115    #[cfg(feature = "std")]
116    pub fn from_stdin() -> Result<Self, std::io::Error> {
117        use std::io::Read;
118
119        let mut input = std::vec::Vec::with_capacity(1024);
120        std::io::stdin().read_to_end(&mut input)?;
121        Ok(Self {
122            content: Some(input.into_boxed_slice()),
123            ..Default::default()
124        })
125    }
126
127    #[cfg(feature = "std")]
128    pub fn to_path(&self) -> Option<std::path::PathBuf> {
129        self.path.to_path()
130    }
131}
132
133#[cfg(feature = "std")]
134impl clap::builder::ValueParserFactory for InputFile {
135    type Parser = InputFileParser;
136
137    fn value_parser() -> Self::Parser {
138        InputFileParser
139    }
140}
141
142#[doc(hidden)]
143#[derive(Clone)]
144#[cfg(feature = "std")]
145pub struct InputFileParser;
146
147#[cfg(feature = "std")]
148impl clap::builder::TypedValueParser for InputFileParser {
149    type Value = InputFile;
150
151    fn parse_ref(
152        &self,
153        _cmd: &clap::Command,
154        _arg: Option<&clap::Arg>,
155        value: &std::ffi::OsStr,
156    ) -> Result<Self::Value, clap::error::Error> {
157        use clap::error::{Error, ErrorKind};
158
159        match value.to_str() {
160            Some("-") => InputFile::from_stdin().map_err(|err| Error::raw(ErrorKind::Io, err)),
161            Some(_) | None => {
162                let path = std::path::PathBuf::from(value);
163                if !path.exists() {
164                    return Err(Error::raw(
165                        ErrorKind::ValueValidation,
166                        format!("invalid input '{}': file does not exist", path.display()),
167                    ));
168                }
169                if path.extension().is_none_or(|extension| !extension.eq_ignore_ascii_case("masp"))
170                {
171                    return Err(Error::raw(
172                        ErrorKind::ValueValidation,
173                        format!(
174                            "invalid input '{}': expected a compiled .masp package",
175                            path.display()
176                        ),
177                    ));
178                }
179                Ok(InputFile::from_path(path))
180            }
181        }
182    }
183}
184
185#[cfg(all(test, feature = "std"))]
186mod tests {
187    use clap::builder::TypedValueParser;
188
189    use super::*;
190
191    #[test]
192    fn parser_accepts_compiled_packages() {
193        let package = tempfile::Builder::new().suffix(".masp").tempfile().unwrap();
194        let input = InputFileParser
195            .parse_ref(&clap::Command::new("test"), None, package.path().as_os_str())
196            .unwrap();
197
198        assert_matches!(input.path.to_path(), Some(path) if path == package.path());
199    }
200}