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