From 0d32a67a5c2c2d5e640b1dbd8155016503e36571 Mon Sep 17 00:00:00 2001 From: Priyanshu Dangare Date: Sun, 20 Sep 2026 23:20:41 +0530 Subject: [PATCH 1/5] fix: reject symlink build outputs before truncation --- Cargo.lock | 2 ++ Cargo.toml | 6 +++++ src/frontend/build.rs | 51 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 55 insertions(+), 4 deletions(-) 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/frontend/build.rs b/src/frontend/build.rs index 7abea3e..6672bd2 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,40 @@ 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. + #[cfg(windows)] + if file.metadata()?.file_type().is_symlink() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "output path is a symlink", + )); + } + file.set_len(0)?; + Ok(file) +} From ee3c15cc6028ca3c7de2bf4e1b4c7a39b29bf117 Mon Sep 17 00:00:00 2001 From: Priyanshu Dangare Date: Sun, 20 Sep 2026 23:20:41 +0530 Subject: [PATCH 2/5] perf: borrow boolean conditions during code generation --- src/codegen/input.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 { From b3a31b6ccb3d15a9f5d87f6cf3464fbf60400b5e Mon Sep 17 00:00:00 2001 From: Priyanshu Dangare Date: Sun, 20 Sep 2026 23:20:41 +0530 Subject: [PATCH 3/5] perf: serialize block field strings directly --- src/codegen/sb3.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) 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<()> { From 61eba01a88b705e433e05001f129312e0dd90d8b Mon Sep 17 00:00:00 2001 From: Priyanshu Dangare Date: Sun, 20 Sep 2026 23:20:41 +0530 Subject: [PATCH 4/5] perf: append keyword arguments without rebuilding positional arguments --- src/visitor/transformations.rs | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) 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 From 8f2ece921457c770138be061438d34b7e7064004 Mon Sep 17 00:00:00 2001 From: Priyanshu Dangare Date: Mon, 21 Sep 2026 00:10:20 +0530 Subject: [PATCH 5/5] fixup! fix: reject symlink build outputs before truncation --- src/frontend/build.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/frontend/build.rs b/src/frontend/build.rs index 6672bd2..09c27a9 100644 --- a/src/frontend/build.rs +++ b/src/frontend/build.rs @@ -55,13 +55,16 @@ fn open_output(path: &Path) -> io::Result { 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 file.metadata()?.file_type().is_symlink() { + if metadata.file_type().is_symlink() { return Err(io::Error::new( io::ErrorKind::InvalidInput, "output path is a symlink", )); } - file.set_len(0)?; + if metadata.is_file() { + file.set_len(0)?; + } Ok(file) }