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#[derive(Debug, thiserror::Error)]
106pub enum InvalidInputError {
107 #[error("invalid input file '{}': unsupported file type", .0.display())]
109 UnsupportedFileType(std::path::PathBuf),
110 #[error("could not detect file type of input")]
112 UnrecognizedFileType,
113 #[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#[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 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 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 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#[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 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}