Fork of daniellemaywood.uk/gleam — Wasm codegen work
14 kB
470 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4pub mod memory;
5
6use crate::error::{Error, FileIoAction, FileKind, Result};
7use async_trait::async_trait;
8use debug_ignore::DebugIgnore;
9use flate2::read::GzDecoder;
10use std::{
11 collections::{HashMap, HashSet, VecDeque},
12 fmt::Debug,
13 io,
14 iter::Extend,
15 time::SystemTime,
16 vec::IntoIter,
17};
18use tar::{Archive, Entry};
19
20use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
21
22/// Validates that a path is safe to use as a relative path.
23pub fn validate_safe_relative_path(path: &Utf8Path) -> Result<(), &'static str> {
24 // Absolute paths are not permitted.
25 // On Windows paths starting with \\ are drive-relative, so absolute as
26 // far as we are concerned.
27 if path.is_absolute() || (cfg!(windows) && path.starts_with("\\")) {
28 return Err("paths must be relative");
29 }
30
31 for component in path.components() {
32 if component == Utf8Component::ParentDir {
33 return Err("paths must not contain .. segments");
34 }
35 }
36
37 Ok(())
38}
39
40/// Takes in a source path and a target path and determines a relative path
41/// from source -> target.
42/// If given a relative target path, no calculation occurs.
43/// # Panics
44/// The provided source path should be absolute, otherwise will panic.
45pub fn make_relative(source_path: &Utf8Path, target_path: &Utf8Path) -> Utf8PathBuf {
46 assert!(source_path.is_absolute());
47 // Input target will always be canonicalised whereas source will not
48 // This causes problems with diffing on windows since canonicalised paths have a special root
49 // As such we are attempting to strip the target path
50 // Based on https://github.com/rust-lang/rust/issues/42869#issuecomment-1712317081
51 #[cfg(target_family = "windows")]
52 let binding = target_path.to_string();
53 #[cfg(target_family = "windows")]
54 let target_path = Utf8Path::new(binding.trim_start_matches(r"\\?\"));
55
56 match target_path.is_absolute() {
57 true => pathdiff::diff_utf8_paths(target_path, source_path)
58 .expect("Should not fail on two absolute paths"),
59
60 false => target_path.into(),
61 }
62}
63
64pub trait Reader: io::Read {
65 /// A wrapper around `std::io::Read` that has Gleam's error handling.
66 fn read_bytes(&mut self, buffer: &mut [u8]) -> Result<usize> {
67 self.read(buffer).map_err(|e| self.convert_err(e))
68 }
69
70 fn convert_err<E: std::error::Error>(&self, error: E) -> Error;
71}
72
73pub trait Utf8Writer: std::fmt::Write {
74 /// A wrapper around `fmt::Write` that has Gleam's error handling.
75 fn str_write(&mut self, str: &str) -> Result<()> {
76 self.write_str(str).map_err(|e| self.convert_err(e))
77 }
78
79 fn convert_err<E: std::error::Error>(&self, err: E) -> Error;
80}
81
82impl Utf8Writer for String {
83 fn convert_err<E: std::error::Error>(&self, error: E) -> Error {
84 Error::FileIo {
85 action: FileIoAction::WriteTo,
86 kind: FileKind::File,
87 path: Utf8PathBuf::from("<in memory>"),
88 err: Some(error.to_string()),
89 }
90 }
91}
92
93pub trait Writer: io::Write + Utf8Writer {
94 /// A wrapper around `io::Write` that has Gleam's error handling.
95 fn write(&mut self, bytes: &[u8]) -> Result<(), Error> {
96 io::Write::write(self, bytes)
97 .map(|_| ())
98 .map_err(|e| self.convert_err(e))
99 }
100}
101
102#[derive(Debug, PartialEq, Eq, Clone)]
103pub enum Content {
104 Binary(Vec<u8>),
105 Text(String),
106}
107
108impl Content {
109 pub fn as_bytes(&self) -> &[u8] {
110 match self {
111 Content::Binary(data) => data,
112 Content::Text(data) => data.as_bytes(),
113 }
114 }
115
116 pub fn text(&self) -> Option<&str> {
117 match self {
118 Content::Binary(_) => None,
119 Content::Text(s) => Some(s),
120 }
121 }
122}
123
124impl From<Vec<u8>> for Content {
125 fn from(bytes: Vec<u8>) -> Self {
126 Content::Binary(bytes)
127 }
128}
129
130impl From<&[u8]> for Content {
131 fn from(bytes: &[u8]) -> Self {
132 Content::Binary(bytes.to_vec())
133 }
134}
135
136impl From<String> for Content {
137 fn from(text: String) -> Self {
138 Content::Text(text)
139 }
140}
141
142impl From<&str> for Content {
143 fn from(text: &str) -> Self {
144 Content::Text(text.to_string())
145 }
146}
147
148#[derive(Debug, PartialEq, Eq, Clone)]
149pub struct OutputFile {
150 pub content: Content,
151 pub path: Utf8PathBuf,
152}
153
154#[derive(Debug)]
155pub struct ReadDir {
156 entries: Vec<io::Result<DirEntry>>,
157}
158
159impl FromIterator<io::Result<DirEntry>> for ReadDir {
160 fn from_iter<I: IntoIterator<Item = io::Result<DirEntry>>>(iter: I) -> Self {
161 ReadDir {
162 entries: iter.into_iter().collect(),
163 }
164 }
165}
166
167impl ReadDir {
168 pub fn extend(mut self, other: ReadDir) -> Self {
169 self.entries.extend(other);
170
171 ReadDir {
172 entries: self.entries,
173 }
174 }
175}
176
177impl IntoIterator for ReadDir {
178 type Item = io::Result<DirEntry>;
179 type IntoIter = IntoIter<Self::Item>;
180
181 fn into_iter(self) -> Self::IntoIter {
182 self.entries.into_iter()
183 }
184}
185
186#[derive(Debug, Clone)]
187pub struct DirEntry {
188 pub pathbuf: Utf8PathBuf,
189}
190
191impl DirEntry {
192 pub fn from_path<P: AsRef<Utf8Path>>(path: P) -> DirEntry {
193 DirEntry {
194 pathbuf: path.as_ref().to_path_buf(),
195 }
196 }
197
198 pub fn from_pathbuf(pathbuf: Utf8PathBuf) -> DirEntry {
199 DirEntry { pathbuf }
200 }
201
202 pub fn as_path(&self) -> &Utf8Path {
203 self.pathbuf.as_path()
204 }
205
206 pub fn into_path(self) -> Utf8PathBuf {
207 self.pathbuf
208 }
209}
210
211/// Structure holding state to walk across a directory's descendant files at
212/// any level. Note that each descendant directory is only visited once
213/// regardless of symlinks, avoiding infinite symlink loops.
214#[derive(Debug, Clone)]
215pub struct DirWalker {
216 walk_queue: VecDeque<Utf8PathBuf>,
217 dirs_walked: im::HashSet<Utf8PathBuf>,
218}
219
220impl DirWalker {
221 /// Create a directory walker starting at the given path.
222 pub fn new(dir: Utf8PathBuf) -> Self {
223 Self {
224 walk_queue: VecDeque::from([dir]),
225 dirs_walked: im::HashSet::new(),
226 }
227 }
228
229 /// Convert this walker to an iterator over file paths.
230 ///
231 /// This iterator calls [`Self::next_file`]. Errors are returned if certain
232 /// directories cannot be read.
233 pub fn into_file_iter(
234 mut self,
235 io: &impl FileSystemReader,
236 ) -> impl Iterator<Item = Result<Utf8PathBuf>> + '_ {
237 std::iter::from_fn(move || self.next_file(io).transpose())
238 }
239
240 /// Advance the directory walker to the next file. The returned path will
241 /// be relative to the starting directory's path, even with symlinks
242 /// (it is not canonicalised).
243 pub fn next_file(&mut self, io: &impl FileSystemReader) -> Result<Option<Utf8PathBuf>> {
244 while let Some(next_path) = self.walk_queue.pop_front() {
245 let real_path = io.canonicalise(&next_path)?;
246
247 if io.is_file(&real_path) {
248 // Return the path relative to the starting directory, not the
249 // canonicalised path (which we only use to check for already
250 // visited directories).
251 return Ok(Some(next_path));
252 }
253
254 // If it's not a directory then it contains no other files, so there's nothing to do.
255 if !io.is_directory(&real_path) {
256 continue;
257 }
258
259 // If we have already processed this directory then we don't need to do it again.
260 // This could be due to symlinks.
261 let already_seen = self.dirs_walked.insert(real_path.clone()).is_some();
262 if already_seen {
263 continue;
264 }
265
266 for entry in io.read_dir(&next_path)? {
267 let Ok(entry) = entry else {
268 return Err(Error::FileIo {
269 kind: FileKind::Directory,
270 action: FileIoAction::Read,
271 path: next_path,
272 err: None,
273 });
274 };
275
276 self.walk_queue.push_back(entry.into_path());
277 }
278 }
279
280 Ok(None)
281 }
282}
283
284/// A trait used to read files.
285/// Typically we use an implementation that reads from the file system,
286/// but in tests and in other places other implementations may be used.
287pub trait FileSystemReader {
288 fn read_dir(&self, path: &Utf8Path) -> Result<ReadDir>;
289 fn read(&self, path: &Utf8Path) -> Result<String, Error>;
290 fn read_bytes(&self, path: &Utf8Path) -> Result<Vec<u8>, Error>;
291 fn reader(&self, path: &Utf8Path) -> Result<WrappedReader, Error>;
292 fn is_file(&self, path: &Utf8Path) -> bool;
293 fn is_same_file(&self, left: &Utf8Path, right: &Utf8Path) -> Result<bool, Error>;
294 fn is_directory(&self, path: &Utf8Path) -> bool;
295 fn modification_time(&self, path: &Utf8Path) -> Result<SystemTime, Error>;
296 fn canonicalise(&self, path: &Utf8Path) -> Result<Utf8PathBuf, Error>;
297}
298
299/// Iterates over files with the given extension in a certain directory.
300/// Symlinks are followed.
301pub fn files_with_extension<'a>(
302 io: &'a impl FileSystemReader,
303 dir: &'a Utf8Path,
304 extension: &'a str,
305) -> impl Iterator<Item = Utf8PathBuf> + 'a {
306 DirWalker::new(dir.to_path_buf())
307 .into_file_iter(io)
308 .filter_map(Result::ok)
309 .filter(|path| path.extension() == Some(extension))
310}
311
312/// A trait used to run other programs.
313pub trait CommandExecutor {
314 fn exec(&self, command: Command) -> Result<i32, Error>;
315}
316
317/// A command one can run with a `CommandExecutor`
318#[derive(Debug, Eq, PartialEq)]
319pub struct Command {
320 pub program: String,
321 pub args: Vec<String>,
322 pub env: Vec<(String, String)>,
323 pub cwd: Option<Utf8PathBuf>,
324 pub stdio: Stdio,
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328pub enum Stdio {
329 Inherit,
330 Null,
331}
332
333impl Stdio {
334 pub fn get_process_stdio(&self) -> std::process::Stdio {
335 match self {
336 Stdio::Inherit => std::process::Stdio::inherit(),
337 Stdio::Null => std::process::Stdio::null(),
338 }
339 }
340}
341
342/// A trait used to compile Erlang and Elixir modules to BEAM bytecode.
343pub trait BeamCompilerIO {
344 fn compile_beam(
345 &self,
346 out: &Utf8Path,
347 lib: &Utf8Path,
348 modules: &HashSet<Utf8PathBuf>,
349 stdio: Stdio,
350 ) -> Result<Vec<String>, Error>;
351}
352
353/// A trait used to write files.
354/// Typically we use an implementation that writes to the file system,
355/// but in tests and in other places other implementations may be used.
356pub trait FileSystemWriter {
357 fn mkdir(&self, path: &Utf8Path) -> Result<(), Error>;
358 fn write(&self, path: &Utf8Path, content: &str) -> Result<(), Error>;
359 fn write_bytes(&self, path: &Utf8Path, content: &[u8]) -> Result<(), Error>;
360 fn delete_directory(&self, path: &Utf8Path) -> Result<(), Error>;
361 fn copy(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error>;
362 fn copy_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error>;
363 fn hardlink(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error>;
364 fn symlink_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error>;
365 fn delete_file(&self, path: &Utf8Path) -> Result<(), Error>;
366 fn exists(&self, path: &Utf8Path) -> bool;
367}
368
369#[derive(Debug)]
370/// A wrapper around a Read implementing object that has Gleam's error handling.
371pub struct WrappedReader {
372 path: Utf8PathBuf,
373 inner: DebugIgnore<Box<dyn io::Read>>,
374}
375
376impl WrappedReader {
377 pub fn new(path: &Utf8Path, inner: Box<dyn io::Read>) -> Self {
378 Self {
379 path: path.to_path_buf(),
380 inner: DebugIgnore(inner),
381 }
382 }
383
384 fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
385 self.inner.read(buffer)
386 }
387}
388
389impl io::Read for WrappedReader {
390 fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
391 self.read(buffer)
392 }
393}
394
395impl Reader for WrappedReader {
396 fn convert_err<E: std::error::Error>(&self, err: E) -> Error {
397 Error::FileIo {
398 kind: FileKind::File,
399 action: FileIoAction::Read,
400 path: self.path.clone(),
401 err: Some(err.to_string()),
402 }
403 }
404}
405
406#[async_trait]
407pub trait HttpClient {
408 async fn send(&self, request: http::Request<Vec<u8>>)
409 -> Result<http::Response<Vec<u8>>, Error>;
410}
411
412pub trait TarUnpacker {
413 // FIXME: The reader types are restrictive here. We should be more generic
414 // than this.
415 fn io_result_entries<'a>(
416 &self,
417 archive: &'a mut Archive<WrappedReader>,
418 ) -> io::Result<tar::Entries<'a, WrappedReader>>;
419
420 fn entries<'a>(
421 &self,
422 archive: &'a mut Archive<WrappedReader>,
423 ) -> Result<tar::Entries<'a, WrappedReader>> {
424 tracing::debug!("iterating through tar archive");
425 self.io_result_entries(archive)
426 .map_err(|e| Error::ExpandTar {
427 error: e.to_string(),
428 })
429 }
430
431 fn io_result_unpack(
432 &self,
433 path: &Utf8Path,
434 archive: Archive<GzDecoder<Entry<'_, WrappedReader>>>,
435 ) -> io::Result<()>;
436
437 fn unpack(
438 &self,
439 path: &Utf8Path,
440 archive: Archive<GzDecoder<Entry<'_, WrappedReader>>>,
441 ) -> Result<()> {
442 tracing::debug!(path = ?path, "unpacking tar archive");
443 self.io_result_unpack(path, archive)
444 .map_err(|e| Error::FileIo {
445 action: FileIoAction::WriteTo,
446 kind: FileKind::Directory,
447 path: path.to_path_buf(),
448 err: Some(e.to_string()),
449 })
450 }
451}
452
453#[inline]
454pub fn is_native_file_extension(extension: &str) -> bool {
455 matches!(
456 extension,
457 "erl" | "hrl" | "ex" | "js" | "mjs" | "cjs" | "jsx" | "ts" | "mts" | "cts" | "tsx"
458 )
459}
460
461pub fn ordered_map<S, K, V>(value: &HashMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
462where
463 S: serde::Serializer,
464 K: serde::Serialize + Ord,
465 V: serde::Serialize,
466{
467 use serde::Serialize;
468 let ordered: std::collections::BTreeMap<_, _> = value.iter().collect();
469 ordered.serialize(serializer)
470}