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 pub fn new(path: impl Into<Uri>, content: Option<Box<[u8]>>) -> Self {
94 Self {
95 path: path.into(),
96 content,
97 }
98 }
99
100 #[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 #[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}