diff --git a/Cargo.lock b/Cargo.lock index 6d2730a..2b8c2ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,7 @@ dependencies = [ "js-sys", "lalrpop", "lalrpop-util", + "libc", "log", "logos", "md-5", @@ -700,6 +701,7 @@ dependencies = [ "tsify", "walkdir", "wasm-bindgen", + "windows-sys 0.60.2", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 7a062fe..dd86a6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,12 @@ console_log = "1.0.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rand = "0.9.1" +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.60", features = ["Win32_Storage_FileSystem"] } + [build-dependencies] lalrpop = "0.22.0" diff --git a/src/codegen/input.rs b/src/codegen/input.rs index 8111715..14ed87e 100644 --- a/src/codegen/input.rs +++ b/src/codegen/input.rs @@ -1,6 +1,9 @@ -use std::io::{ - self, - Write, +use std::{ + borrow::Cow, + io::{ + self, + Write, + }, }; use serde_json::json; @@ -67,11 +70,11 @@ pub fn is_expr_boolean(expr: &Expr, s: S) -> bool { ) } -pub fn coerce_condition(expr: &Expr, s: S) -> Expr { +pub fn coerce_condition<'a>(expr: &'a Expr, s: S) -> Cow<'a, Expr> { if is_expr_boolean(expr, s) { - return expr.clone(); + return Cow::Borrowed(expr); } - BinOp::Eq.to_expr(0..0, expr.clone(), Value::from(true).to_expr(0..0)) + Cow::Owned(BinOp::Eq.to_expr(0..0, expr.clone(), Value::from(true).to_expr(0..0))) } impl Sb3 { diff --git a/src/codegen/sb3.rs b/src/codegen/sb3.rs index de01895..52f37b2 100644 --- a/src/codegen/sb3.rs +++ b/src/codegen/sb3.rs @@ -369,20 +369,17 @@ impl Sb3 { } pub fn single_field(&mut self, name: &'static str, value: &str) -> io::Result<()> { - write!( - self.json, - r#","fields":{{"{name}":[{},null]}}"#, - json!(value) - ) + write!(self.json, r#","fields":{{"{name}":["#)?; + serde_json::to_writer(&mut self.json, value)?; + self.json.write_all(b",null]}") } pub fn single_field_id(&mut self, name: &'static str, value: &str) -> io::Result<()> { - write!( - self.json, - r#","fields":{{"{name}":[{},{}]}}"#, - json!(value), - json!(value) - ) + write!(self.json, r#","fields":{{"{name}":["#)?; + serde_json::to_writer(&mut self.json, value)?; + self.json.write_all(b",")?; + serde_json::to_writer(&mut self.json, value)?; + self.json.write_all(b"]}") } pub fn substack(&mut self, name: &str, this_id: Option) -> io::Result<()> { diff --git a/src/frontend/build.rs b/src/frontend/build.rs index 7abea3e..09c27a9 100644 --- a/src/frontend/build.rs +++ b/src/frontend/build.rs @@ -1,9 +1,18 @@ use std::{ cell::RefCell, env, - fs::File, - io::BufWriter, - path::PathBuf, + fs::{ + File, + OpenOptions, + }, + io::{ + self, + BufWriter, + }, + path::{ + Path, + PathBuf, + }, rc::Rc, }; @@ -19,6 +28,43 @@ pub fn build(input: Option, output: Option) -> anyhow::Result< let project_name = canonical_input.file_name().unwrap().to_str().unwrap(); let output = output.unwrap_or_else(|| input.join(format!("{project_name}.sb3"))); let fs = Rc::new(RefCell::new(RealFS)); - let file = BufWriter::new(File::create(&output)?); + let file = BufWriter::new(open_output(&output)?); build_impl(fs, canonical_input, file, None) } + +fn open_output(path: &Path) -> io::Result { + let mut options = OpenOptions::new(); + options.write(true).create(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.custom_flags(libc::O_NOFOLLOW); + } + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + + use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT; + options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT); + } + #[cfg(not(any(unix, windows)))] + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "opening output without following symlinks is unsupported", + )); + + let file = options.open(path)?; + // Inspect and truncate the same handle; never check the path before opening it. + let metadata = file.metadata()?; + #[cfg(windows)] + if metadata.file_type().is_symlink() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "output path is a symlink", + )); + } + if metadata.is_file() { + file.set_len(0)?; + } + Ok(file) +} diff --git a/src/visitor/transformations.rs b/src/visitor/transformations.rs index 2397e4c..c25f7b4 100644 --- a/src/visitor/transformations.rs +++ b/src/visitor/transformations.rs @@ -514,37 +514,22 @@ pub fn keyword_arguments( d: D, ) { if let Some(sig) = signature { - // Build a new vector of arguments in the order given by the signature. - let mut new_args = Vec::with_capacity(sig.len()); - let mut pos = 0; + let positional_count = args.len(); - for param in sig { - if pos < args.len() { + for (index, param) in sig.iter().enumerate() { + if index < positional_count { // If there is both a positional and keyword argument, we prefer the positional one. - // Remove the keyword argument from the map. kwargs.remove(¶m.name); - // Use the next positional argument. - new_args.push(args[pos].clone()); - pos += 1; } else if let Some((_, kw_expr)) = kwargs.remove(¶m.name) { // No more positional args, but there is a matching keyword argument. - new_args.push(kw_expr); + args.push(kw_expr); } else if let Some(default) = ¶m.default { // Compute the default value if one is provided. - new_args.push(default.clone().into()); + args.push(default.clone().into()); } // If no positional, keyword, or default value exists, then // we simply do not insert anything (and no error is raised). } - - // Append any extra positional arguments that exceed the signature length. - while pos < args.len() { - new_args.push(args[pos].clone()); - pos += 1; - } - - // Replace the original args with the re-ordered version. - *args = new_args; } // Generate diagnostics for any remaining unknown keyword arguments