midenc_session/
inputs.rs

1use alloc::{borrow::Cow, format, string::String, vec, vec::Vec};
2use core::fmt;
3use std::{
4    ffi::OsStr,
5    path::{Path, PathBuf},
6};
7
8#[derive(Clone)]
9pub struct FileName {
10    name: Cow<'static, str>,
11    is_path: bool,
12}
13impl Eq for FileName {}
14impl PartialEq for FileName {
15    fn eq(&self, other: &Self) -> bool {
16        self.name == other.name
17    }
18}
19impl PartialOrd for FileName {
20    #[inline]
21    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
22        Some(self.cmp(other))
23    }
24}
25impl Ord for FileName {
26    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
27        self.name.cmp(&other.name)
28    }
29}
30impl fmt::Debug for FileName {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(f, "{}", self.as_str())
33    }
34}
35impl fmt::Display for FileName {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "{}", self.as_str())
38    }
39}
40#[cfg(feature = "std")]
41impl AsRef<std::path::Path> for FileName {
42    fn as_ref(&self) -> &std::path::Path {
43        std::path::Path::new(self.name.as_ref())
44    }
45}
46#[cfg(feature = "std")]
47impl From<std::path::PathBuf> for FileName {
48    fn from(path: std::path::PathBuf) -> Self {
49        Self {
50            name: path.to_string_lossy().into_owned().into(),
51            is_path: true,
52        }
53    }
54}
55impl From<&'static str> for FileName {
56    fn from(name: &'static str) -> Self {
57        Self {
58            name: Cow::Borrowed(name),
59            is_path: false,
60        }
61    }
62}
63impl From<String> for FileName {
64    fn from(name: String) -> Self {
65        Self {
66            name: Cow::Owned(name),
67            is_path: false,
68        }
69    }
70}
71impl AsRef<str> for FileName {
72    fn as_ref(&self) -> &str {
73        self.name.as_ref()
74    }
75}
76impl FileName {
77    pub fn is_path(&self) -> bool {
78        self.is_path
79    }
80
81    #[cfg(feature = "std")]
82    pub fn as_path(&self) -> &std::path::Path {
83        self.as_ref()
84    }
85
86    pub fn as_str(&self) -> &str {
87        self.name.as_ref()
88    }
89
90    #[cfg(feature = "std")]
91    pub fn file_name(&self) -> Option<&str> {
92        self.as_path().file_name().and_then(|name| name.to_str())
93    }
94
95    #[cfg(not(feature = "std"))]
96    pub fn file_name(&self) -> Option<&str> {
97        match self.name.rsplit_once('/') {
98            Some((_, name)) => Some(name),
99            None => Some(self.name.as_ref()),
100        }
101    }
102}
103
104/// An error that occurs when detecting the file type of an input
105#[derive(Debug, thiserror::Error)]
106pub enum InvalidInputError {
107    /// Occurs if an unsupported file type is given as an input
108    #[error("invalid input file '{}': unsupported file type", .0.display())]
109    UnsupportedFileType(std::path::PathBuf),
110    /// We attempted to detecth the file type from the raw bytes, but failed
111    #[error("could not detect file type of input")]
112    UnrecognizedFileType,
113    /// Unable to read input file
114    #[error(transparent)]
115    Io(#[from] std::io::Error),
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum InputType {
120    Real(PathBuf),
121    Stdin { name: FileName, input: Vec<u8> },
122}
123
124/// This enum represents the types of raw inputs provided to the compiler
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct InputFile {
127    pub file: InputType,
128    file_type: FileType,
129}
130impl InputFile {
131    pub fn new(ty: FileType, file: InputType) -> Self {
132        Self {
133            file,
134            file_type: ty,
135        }
136    }
137
138    /// Returns an [InputFile] representing an empty WebAssembly module binary
139    pub fn empty() -> Self {
140        Self {
141            file: InputType::Stdin {
142                name: "empty".into(),
143                input: vec![],
144            },
145            file_type: FileType::Wasm,
146        }
147    }
148
149    /// Get an [InputFile] representing the contents of `path`.
150    ///
151    /// This function returns an error if the contents are not a valid supported file type.
152    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, InvalidInputError> {
153        let path = path.as_ref();
154        let file_type = FileType::try_from(path)?;
155        Ok(Self {
156            file: InputType::Real(path.to_path_buf()),
157            file_type,
158        })
159    }
160
161    /// Get an [InputFile] representing the contents received from standard input.
162    ///
163    /// This function returns an error if the contents are not a valid supported file type.
164    pub fn from_stdin(name: FileName) -> Result<Self, InvalidInputError> {
165        use std::io::Read;
166
167        let mut input = Vec::with_capacity(1024);
168        std::io::stdin().read_to_end(&mut input)?;
169        Self::from_bytes(input, name)
170    }
171
172    pub fn from_bytes(bytes: Vec<u8>, name: FileName) -> Result<Self, InvalidInputError> {
173        let file_type = FileType::detect(&bytes)?;
174        Ok(Self {
175            file: InputType::Stdin { name, input: bytes },
176            file_type,
177        })
178    }
179
180    pub fn file_type(&self) -> FileType {
181        self.file_type
182    }
183
184    pub fn file_name(&self) -> FileName {
185        match &self.file {
186            InputType::Real(ref path) => path.clone().into(),
187            InputType::Stdin { name, .. } => name.clone(),
188        }
189    }
190
191    pub fn as_path(&self) -> Option<&Path> {
192        match &self.file {
193            InputType::Real(ref path) => Some(path),
194            _ => None,
195        }
196    }
197
198    pub fn is_real(&self) -> bool {
199        matches!(self.file, InputType::Real(_))
200    }
201
202    pub fn filestem(&self) -> &str {
203        match &self.file {
204            InputType::Real(ref path) => path.file_stem().unwrap().to_str().unwrap(),
205            InputType::Stdin { .. } => "noname",
206        }
207    }
208}
209impl clap::builder::ValueParserFactory for InputFile {
210    type Parser = InputFileParser;
211
212    fn value_parser() -> Self::Parser {
213        InputFileParser
214    }
215}
216
217#[doc(hidden)]
218#[derive(Clone)]
219pub struct InputFileParser;
220impl clap::builder::TypedValueParser for InputFileParser {
221    type Value = InputFile;
222
223    fn parse_ref(
224        &self,
225        _cmd: &clap::Command,
226        _arg: Option<&clap::Arg>,
227        value: &OsStr,
228    ) -> Result<Self::Value, clap::error::Error> {
229        use clap::error::{Error, ErrorKind};
230
231        let input_file = match value.to_str() {
232            Some("-") => InputFile::from_stdin("stdin".into()).map_err(|err| match err {
233                InvalidInputError::Io(err) => Error::raw(ErrorKind::Io, err),
234                err => Error::raw(ErrorKind::ValueValidation, err),
235            })?,
236            Some(_) | None => {
237                InputFile::from_path(PathBuf::from(value)).map_err(|err| match err {
238                    InvalidInputError::Io(err) => Error::raw(ErrorKind::Io, err),
239                    err => Error::raw(ErrorKind::ValueValidation, err),
240                })?
241            }
242        };
243
244        match &input_file.file {
245            InputType::Real(path) => {
246                if path.exists() {
247                    if path.is_file() {
248                        Ok(input_file)
249                    } else {
250                        Err(Error::raw(
251                            ErrorKind::ValueValidation,
252                            format!("invalid input '{}': not a file", path.display()),
253                        ))
254                    }
255                } else {
256                    Err(Error::raw(
257                        ErrorKind::ValueValidation,
258                        format!("invalid input '{}': file does not exist", path.display()),
259                    ))
260                }
261            }
262            InputType::Stdin { .. } => Ok(input_file),
263        }
264    }
265}
266
267/// This represents the file types recognized by the compiler
268#[derive(Debug, Copy, Clone, PartialEq, Eq)]
269pub enum FileType {
270    Hir,
271    Masm,
272    Mast,
273    Masp,
274    Wasm,
275    Wat,
276}
277impl fmt::Display for FileType {
278    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279        match self {
280            Self::Hir => f.write_str("hir"),
281            Self::Masm => f.write_str("masm"),
282            Self::Mast => f.write_str("mast"),
283            Self::Masp => f.write_str("masp"),
284            Self::Wasm => f.write_str("wasm"),
285            Self::Wat => f.write_str("wat"),
286        }
287    }
288}
289impl FileType {
290    pub fn detect(bytes: &[u8]) -> Result<Self, InvalidInputError> {
291        if bytes.starts_with(b"\0asm") {
292            return Ok(FileType::Wasm);
293        }
294
295        if bytes.starts_with(b"MAST\0") {
296            return Ok(FileType::Mast);
297        }
298
299        if bytes.starts_with(b"MASP\0") {
300            return Ok(FileType::Masp);
301        }
302
303        fn is_masm_top_level_item(line: &str) -> bool {
304            line.starts_with("const.") || line.starts_with("export.") || line.starts_with("proc.")
305        }
306
307        if let Ok(content) = core::str::from_utf8(bytes) {
308            // Skip comment lines and empty lines
309            let first_line = content
310                .lines()
311                .find(|line| !line.starts_with(['#', ';']) && !line.trim().is_empty());
312            if let Some(first_line) = first_line {
313                if first_line.starts_with("(module #") {
314                    return Ok(FileType::Hir);
315                }
316                if first_line.starts_with("(module") {
317                    return Ok(FileType::Wat);
318                }
319                if is_masm_top_level_item(first_line) {
320                    return Ok(FileType::Masm);
321                }
322            }
323        }
324
325        Err(InvalidInputError::UnrecognizedFileType)
326    }
327}
328impl TryFrom<&Path> for FileType {
329    type Error = InvalidInputError;
330
331    fn try_from(path: &Path) -> Result<Self, Self::Error> {
332        match path.extension().and_then(|ext| ext.to_str()) {
333            Some("hir") => Ok(FileType::Hir),
334            Some("masm") => Ok(FileType::Masm),
335            Some("masl") | Some("mast") => Ok(FileType::Mast),
336            Some("masp") => Ok(FileType::Masp),
337            Some("wasm") => Ok(FileType::Wasm),
338            Some("wat") => Ok(FileType::Wat),
339            _ => Err(InvalidInputError::UnsupportedFileType(path.to_path_buf())),
340        }
341    }
342}