Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

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}