diff --git a/.buildkite/scripts/upload-release.sh b/.buildkite/scripts/upload-release.sh index fa5a2db11a143b..45fa269b636f72 100755 --- a/.buildkite/scripts/upload-release.sh +++ b/.buildkite/scripts/upload-release.sh @@ -190,8 +190,6 @@ function create_release() { local artifacts=( bun-darwin-aarch64.zip bun-darwin-aarch64-profile.zip - bun-darwin-x64.zip - bun-darwin-x64-profile.zip bun-linux-aarch64.zip bun-linux-aarch64-profile.zip bun-linux-x64.zip diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 3b6635febdfdd0..24ec57d409bf4b 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -576,6 +576,7 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "build.zig") set(BUN_USOCKETS_SOURCE ${CWD}/packages/bun-usockets) +# hand written cpp source files. Full list of "source" code (including codegen) is in BUN_CPP_SOURCES file(GLOB BUN_CXX_SOURCES ${CONFIGURE_DEPENDS} ${CWD}/src/io/*.cpp ${CWD}/src/bun.js/modules/*.cpp @@ -632,6 +633,7 @@ register_command( list(APPEND BUN_CPP_SOURCES ${BUN_C_SOURCES} ${BUN_CXX_SOURCES} + ${BUN_ERROR_CODE_OUTPUTS} ${VENDOR_PATH}/picohttpparser/picohttpparser.c ${NODEJS_HEADERS_PATH}/include/node/node_version.h ${BUN_ZIG_GENERATED_CLASSES_OUTPUTS} @@ -890,7 +892,7 @@ if(LINUX) -Wl,--wrap=statx ) endif() - + if(ARCH STREQUAL "x64") target_link_options(${bun} PUBLIC -Wl,--wrap=fcntl diff --git a/packages/bun-build-mdx-rs/src/lib.rs b/packages/bun-build-mdx-rs/src/lib.rs index 4b93e6037fc427..b0859b97ee5dcf 100644 --- a/packages/bun-build-mdx-rs/src/lib.rs +++ b/packages/bun-build-mdx-rs/src/lib.rs @@ -1,55 +1,25 @@ -use bun_native_plugin::{define_bun_plugin, BunLoader, OnBeforeParse}; +use bun_native_plugin::{anyhow, bun, define_bun_plugin, BunLoader, Result}; use mdxjs::{compile, Options as CompileOptions}; use napi_derive::napi; -#[macro_use] -extern crate napi; - define_bun_plugin!("bun-mdx-rs"); -#[no_mangle] -pub extern "C" fn bun_mdx_rs( - args: *const bun_native_plugin::sys::OnBeforeParseArguments, - result: *mut bun_native_plugin::sys::OnBeforeParseResult, -) { - let args = unsafe { &*args }; - - let mut handle = match OnBeforeParse::from_raw(args, result) { - Ok(handle) => handle, - Err(_) => { - return; - } - }; - - let source_str = match handle.input_source_code() { - Ok(source_str) => source_str, - Err(_) => { - handle.log_error("Failed to fetch source code"); - return; - } - }; +#[bun] +pub fn bun_mdx_rs(handle: &mut OnBeforeParse) -> Result<()> { + let source_str = handle.input_source_code()?; let mut options = CompileOptions::gfm(); // Leave it as JSX for Bun to handle options.jsx = true; - let path = match handle.path() { - Ok(path) => path, - Err(e) => { - handle.log_error(&format!("Failed to get path: {:?}", e)); - return; - } - }; + let path = handle.path()?; options.filepath = Some(path.to_string()); - match compile(&source_str, &options) { - Ok(compiled) => { - handle.set_output_source_code(compiled, BunLoader::BUN_LOADER_JSX); - } - Err(_) => { - handle.log_error("Failed to compile MDX"); - return; - } - } + let jsx = compile(&source_str, &options) + .map_err(|e| anyhow::anyhow!("Failed to compile MDX: {:?}", e))?; + + handle.set_output_source_code(jsx, BunLoader::BUN_LOADER_JSX); + + Ok(()) } diff --git a/packages/bun-native-plugin-rs/Cargo.lock b/packages/bun-native-plugin-rs/Cargo.lock index 202700fa3a3b56..0c786953f7489a 100644 --- a/packages/bun-native-plugin-rs/Cargo.lock +++ b/packages/bun-native-plugin-rs/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + [[package]] name = "bindgen" version = "0.70.1" @@ -37,11 +43,24 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bun-macro" +version = "0.1.0" +dependencies = [ + "anyhow", + "napi", + "quote", + "syn", +] + [[package]] name = "bun-native-plugin" version = "0.1.0" dependencies = [ + "anyhow", "bindgen", + "bun-macro", + "napi", ] [[package]] @@ -70,6 +89,25 @@ dependencies = [ "libloading", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "either" version = "1.13.0" @@ -125,6 +163,55 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "napi" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b" +dependencies = [ + "bitflags", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + [[package]] name = "nom" version = "7.1.3" @@ -135,6 +222,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "prettyplease" version = "0.2.25" @@ -221,6 +314,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/packages/bun-native-plugin-rs/Cargo.toml b/packages/bun-native-plugin-rs/Cargo.toml index bf4d7b784b920c..55476cbd837ab9 100644 --- a/packages/bun-native-plugin-rs/Cargo.toml +++ b/packages/bun-native-plugin-rs/Cargo.toml @@ -5,3 +5,13 @@ edition = "2021" [build-dependencies] bindgen = "0.70.1" + +[dependencies] +anyhow = "1.0.94" +bun-macro = { path = "./bun-macro" } +napi = { version = "2.14.1", default-features = false, features = ["napi4"] } + +[features] +default = ["napi"] +napi = [] + diff --git a/packages/bun-native-plugin-rs/README.md b/packages/bun-native-plugin-rs/README.md index f235849872f7bd..6c57a2c9d1a703 100644 --- a/packages/bun-native-plugin-rs/README.md +++ b/packages/bun-native-plugin-rs/README.md @@ -4,7 +4,7 @@ This crate provides a Rustified wrapper over the Bun's native bundler plugin C API. -Some advantages to _native_ bundler plugins as opposed to regular ones implemented in JS: +Some advantages to _native_ bundler plugins as opposed to regular ones implemented in JS are: - Native plugins take full advantage of Bun's parallelized bundler pipeline and run on multiple threads at the same time - Unlike JS, native plugins don't need to do the UTF-8 <-> UTF-16 source code string conversions @@ -30,61 +30,84 @@ Then install this crate: cargo add bun-native-plugin ``` -Now, inside the `lib.rs` file, expose a C ABI function which has the same function signature as the plugin lifecycle hook that you want to implement. +Now, inside the `lib.rs` file, we'll use the `bun_native_plugin::bun` proc macro to define a function which +will implement our native plugin. -For example, implementing `onBeforeParse`: +Here's an example implementing the `onBeforeParse` hook: ```rs -use bun_native_plugin::{define_bun_plugin, OnBeforeParse}; +use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader}; use napi_derive::napi; -/// Define with the name of the plugin +/// Define the plugin and its name define_bun_plugin!("replace-foo-with-bar"); -/// This is necessary for napi-rs to compile this into a proper NAPI module -#[napi] -pub fn register_bun_plugin() {} - -/// Use `no_mangle` so that we can reference this symbol by name later -/// when registering this native plugin in JS. +/// Here we'll implement `onBeforeParse` with code that replaces all occurences of +/// `foo` with `bar`. /// -/// Here we'll create a dummy plugin which replaces all occurences of -/// `foo` with `bar` -#[no_mangle] -pub extern "C" fn on_before_parse_plugin_impl( +/// We use the #[bun] macro to generate some of the boilerplate code. +/// +/// The argument of the function (`handle: &mut OnBeforeParse`) tells +/// the macro that this function implements the `onBeforeParse` hook. +#[bun] +pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> { + // Fetch the input source code. + let input_source_code = handle.input_source_code()?; + + // Get the Loader for the file + let loader = handle.output_loader(); + + + let output_source_code = input_source_code.replace("foo", "bar"); + + handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX); + + Ok(()) +} +``` + +Internally, the `#[bun]` macro wraps your code and declares a C ABI function which implements +the function signature of `onBeforeParse` plugins in Bun's C API for bundler plugins. + +Then it calls your code. The wrapper looks _roughly_ like this: + +```rs +pub extern "C" fn replace_foo_with_bar( args: *const bun_native_plugin::sys::OnBeforeParseArguments, result: *mut bun_native_plugin::sys::OnBeforeParseResult, ) { + // The actual code you wrote is inlined here + fn __replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> { + // Fetch the input source code. + let input_source_code = handle.input_source_code()?; + + // Get the Loader for the file + let loader = handle.output_loader(); + + + let output_source_code = input_source_code.replace("foo", "bar"); + + handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX); + + Ok(()) + } + let args = unsafe { &*args }; - // This returns a handle which is a safe wrapper over the raw - // C API. let mut handle = OnBeforeParse::from_raw(args, result) { Ok(handle) => handle, Err(_) => { - // `OnBeforeParse::from_raw` handles error logging - // so it fine to return here. - return; - } - }; - - let input_source_code = match handle.input_source_code() { - Ok(source_str) => source_str, - Err(_) => { - // If we encounter an error, we must log it so that - // Bun knows this plugin failed. - handle.log_error("Failed to fetch source code!"); return; } }; - let loader = handle.output_loader(); - let output_source_code = source_str.replace("foo", "bar"); - handle.set_output_source_code(output_source_code, loader); + if let Err(e) = __replace_fo_with_bar(&handle) { + handle.log_err(&e.to_string()); + } } ``` -Then compile this NAPI module. If you using napi-rs, the `package.json` should have a `build` script you can run: +Now, let's compile this NAPI module. If you're using napi-rs, the `package.json` should have a `build` script you can run: ```bash bun run build @@ -107,7 +130,7 @@ const result = await Bun.build({ // We tell it to use function we implemented inside of our `lib.rs` code. build.onBeforeParse( { filter: /\.ts/ }, - { napiModule, symbol: "on_before_parse_plugin_impl" }, + { napiModule, symbol: "replace_foo_with_bar" }, ); }, }, @@ -119,19 +142,14 @@ const result = await Bun.build({ ### Error handling and panics -It is highly recommended to avoid panicking as this will crash the runtime. Instead, you must handle errors and log them: +In the case that the value of the `Result` your plugin function returns is an `Err(...)`, the error will be logged to Bun's bundler. -```rs -let input_source_code = match handle.input_source_code() { - Ok(source_str) => source_str, - Err(_) => { - // If we encounter an error, we must log it so that - // Bun knows this plugin failed. - handle.log_error("Failed to fetch source code!"); - return; - } -}; -``` +It is highly advised that you return all errors and avoid `.unwrap()`'ing or `.expecting()`'ing results. + +The `#[bun]` wrapper macro actually runs your code inside of a [`panic::catch_unwind`](https://doc.rust-lang.org/std/panic/fn.catch_unwind.html), +which may catch _some_ panics but **not guaranteed to catch all panics**. + +Therefore, it is recommended to **avoid panics at all costs**. ### Passing state to and from JS: `External` @@ -199,41 +217,16 @@ console.log("Total `foo`s encountered: ", pluginState.getFooCount()); Finally, from the native implementation of your plugin, you can extract the external: ```rs -pub extern "C" fn on_before_parse_plugin_impl( - args: *const bun_native_plugin::sys::OnBeforeParseArguments, - result: *mut bun_native_plugin::sys::OnBeforeParseResult, -) { - let args = unsafe { &*args }; - - let mut handle = OnBeforeParse::from_raw(args, result) { - Ok(handle) => handle, - Err(_) => { - // `OnBeforeParse::from_raw` handles error logging - // so it fine to return here. - return; - } - }; - - let plugin_state: &PluginState = +#[bun] +pub fn on_before_parse_plugin_impl(handle: &mut OnBeforeParse) { // This operation is only safe if you pass in an external when registering the plugin. // If you don't, this could lead to a segfault or access of undefined memory. - match unsafe { handle.external().and_then(|state| state.ok_or(Error::Unknown)) } { - Ok(state) => state, - Err(_) => { - handle.log_error("Failed to get external!"); - return; - } - }; + let plugin_state: &PluginState = + unsafe { handle.external().and_then(|state| state.ok_or(Error::Unknown))? }; // Fetch our source code again - let input_source_code = match handle.input_source_code() { - Ok(source_str) => source_str, - Err(_) => { - handle.log_error("Failed to fetch source code!"); - return; - } - }; + let input_source_code = handle.input_source_code()?; // Count the number of `foo`s and add it to our state let foo_count = source_code.matches("foo").count() as u32; @@ -243,6 +236,6 @@ pub extern "C" fn on_before_parse_plugin_impl( ### Concurrency -Your `extern "C"` plugin function can be called _on any thread_ at _any time_ and _multiple times at once_. +Your plugin function can be called _on any thread_ at _any time_ and possibly _multiple times at once_. -Therefore, you must design any state management to be threadsafe +Therefore, you must design any state management to be threadsafe. diff --git a/packages/bun-native-plugin-rs/bun-macro/Cargo.toml b/packages/bun-native-plugin-rs/bun-macro/Cargo.toml new file mode 100644 index 00000000000000..f7491dee4b287b --- /dev/null +++ b/packages/bun-native-plugin-rs/bun-macro/Cargo.toml @@ -0,0 +1,14 @@ + +[package] +name = "bun-macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +napi = "2.16.13" +anyhow = "1.0.94" \ No newline at end of file diff --git a/packages/bun-native-plugin-rs/bun-macro/src/lib.rs b/packages/bun-native-plugin-rs/bun-macro/src/lib.rs new file mode 100644 index 00000000000000..efedbef86e2f15 --- /dev/null +++ b/packages/bun-native-plugin-rs/bun-macro/src/lib.rs @@ -0,0 +1,54 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Ident, ItemFn}; + +#[proc_macro_attribute] +pub fn bun(_attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input function + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let inner_fn_name = Ident::new(&format!("__{}", fn_name), fn_name.span()); + let fn_block = &input_fn.block; + + // Generate the wrapped function + let output = quote! { + #[no_mangle] + pub unsafe extern "C" fn #fn_name( + args_raw: *mut bun_native_plugin::sys::OnBeforeParseArguments, + result: *mut bun_native_plugin::sys::OnBeforeParseResult, + ) { + fn #inner_fn_name(handle: &mut bun_native_plugin::OnBeforeParse) -> Result<()> { + #fn_block + } + + let args_path = unsafe { (*args_raw).path_ptr }; + let args_path_len = unsafe { (*args_raw).path_len }; + let result_pointer = result; + + let result = std::panic::catch_unwind(|| { + let mut handle = match bun_native_plugin::OnBeforeParse::from_raw(args_raw, result) { + Ok(handle) => handle, + Err(_) => return, + }; + if let Err(e) = #inner_fn_name(&mut handle) { + handle.log_error(&format!("{:?}", e)); + } + }); + + if let Err(e) = result { + let msg_string = format!("Plugin crashed: {:?}", e); + let mut log_options = bun_native_plugin::log_from_message_and_level( + &msg_string, + bun_native_plugin::sys::BunLogLevel::BUN_LOG_LEVEL_ERROR, + args_path, + args_path_len, + ); + unsafe { + ((*result_pointer).log.unwrap())(args_raw, &mut log_options); + } + } + } + }; + + output.into() +} diff --git a/packages/bun-native-plugin-rs/src/lib.rs b/packages/bun-native-plugin-rs/src/lib.rs index 3e589e3bcd3e39..1a8f85941cbab3 100644 --- a/packages/bun-native-plugin-rs/src/lib.rs +++ b/packages/bun-native-plugin-rs/src/lib.rs @@ -244,10 +244,11 @@ //! Your `extern "C"` plugin function can be called _on any thread_ at _any time_ and _multiple times at once_. //! //! Therefore, you must design any state management to be threadsafe - #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] +pub use anyhow; +pub use bun_macro::bun; #[repr(transparent)] pub struct BunPluginName(*const c_char); @@ -261,7 +262,7 @@ impl BunPluginName { #[macro_export] macro_rules! define_bun_plugin { ($name:expr) => { - pub static BUN_PLUGIN_NAME_STRING: &str = $name; + pub static BUN_PLUGIN_NAME_STRING: &str = concat!($name, "\0"); #[no_mangle] pub static BUN_PLUGIN_NAME: bun_native_plugin::BunPluginName = @@ -279,7 +280,9 @@ use std::{ borrow::Cow, cell::UnsafeCell, ffi::{c_char, c_void}, + marker::PhantomData, str::Utf8Error, + sync::PoisonError, }; pub mod sys { @@ -323,7 +326,7 @@ impl Drop for SourceCodeContext { pub type BunLogLevel = sys::BunLogLevel; pub type BunLoader = sys::BunLoader; -fn get_from_raw_str<'a>(ptr: *const u8, len: usize) -> Result> { +fn get_from_raw_str<'a>(ptr: *const u8, len: usize) -> PluginResult> { let slice: &'a [u8] = unsafe { std::slice::from_raw_parts(ptr, len) }; // Windows allows invalid UTF-16 strings in the filesystem. These get converted to WTF-8 in Zig. @@ -351,9 +354,31 @@ pub enum Error { IncompatiblePluginVersion, ExternalTypeMismatch, Unknown, + LockPoisoned, +} + +pub type PluginResult = std::result::Result; +pub type Result = anyhow::Result; + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } } -pub type Result = std::result::Result; +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } + + fn description(&self) -> &str { + "description() is deprecated; use Display" + } + + fn cause(&self) -> Option<&dyn std::error::Error> { + self.source() + } +} impl From for Error { fn from(value: Utf8Error) -> Self { @@ -361,6 +386,12 @@ impl From for Error { } } +impl From> for Error { + fn from(_: PoisonError) -> Self { + Self::LockPoisoned + } +} + /// A safe handle for the arguments + result struct for the /// `OnBeforeParse` bundler lifecycle hook. /// @@ -370,9 +401,10 @@ impl From for Error { /// /// To initialize this struct, see the `from_raw` method. pub struct OnBeforeParse<'a> { - args_raw: &'a sys::OnBeforeParseArguments, + pub args_raw: *mut sys::OnBeforeParseArguments, result_raw: *mut sys::OnBeforeParseResult, compilation_context: *mut SourceCodeContext, + __phantom: PhantomData<&'a ()>, } impl<'a> OnBeforeParse<'a> { @@ -394,10 +426,10 @@ impl<'a> OnBeforeParse<'a> { /// } /// ``` pub fn from_raw( - args: &'a sys::OnBeforeParseArguments, + args: *mut sys::OnBeforeParseArguments, result: *mut sys::OnBeforeParseResult, - ) -> Result { - if args.__struct_size < std::mem::size_of::() + ) -> PluginResult { + if unsafe { (*args).__struct_size } < std::mem::size_of::() || unsafe { (*result).__struct_size } < std::mem::size_of::() { let message = "This plugin is not compatible with the current version of Bun."; @@ -405,8 +437,8 @@ impl<'a> OnBeforeParse<'a> { __struct_size: std::mem::size_of::(), message_ptr: message.as_ptr(), message_len: message.len(), - path_ptr: args.path_ptr, - path_len: args.path_len, + path_ptr: unsafe { (*args).path_ptr }, + path_len: unsafe { (*args).path_len }, source_line_text_ptr: std::ptr::null(), source_line_text_len: 0, level: BunLogLevel::BUN_LOG_LEVEL_ERROR as i8, @@ -426,15 +458,21 @@ impl<'a> OnBeforeParse<'a> { args_raw: args, result_raw: result, compilation_context: std::ptr::null_mut() as *mut _, + __phantom: Default::default(), }) } - pub fn path(&self) -> Result> { - get_from_raw_str(self.args_raw.path_ptr, self.args_raw.path_len) + pub fn path(&self) -> PluginResult> { + unsafe { get_from_raw_str((*self.args_raw).path_ptr, (*self.args_raw).path_len) } } - pub fn namespace(&self) -> Result> { - get_from_raw_str(self.args_raw.namespace_ptr, self.args_raw.namespace_len) + pub fn namespace(&self) -> PluginResult> { + unsafe { + get_from_raw_str( + (*self.args_raw).namespace_ptr, + (*self.args_raw).namespace_len, + ) + } } /// Get the external object from the `OnBeforeParse` arguments. @@ -485,12 +523,13 @@ impl<'a> OnBeforeParse<'a> { /// }, /// }; /// ``` - pub unsafe fn external(&self) -> Result> { - if self.args_raw.external.is_null() { + pub unsafe fn external(&self) -> PluginResult> { + if unsafe { (*self.args_raw).external.is_null() } { return Ok(None); } - let external: *mut TaggedObject = self.args_raw.external as *mut TaggedObject; + let external: *mut TaggedObject = + unsafe { (*self.args_raw).external as *mut TaggedObject }; unsafe { if (*external).type_id != TypeId::of::() { @@ -505,12 +544,13 @@ impl<'a> OnBeforeParse<'a> { /// /// This is unsafe as you must ensure that no other invocation of the plugin /// simultaneously holds a mutable reference to the external. - pub unsafe fn external_mut(&mut self) -> Result> { - if self.args_raw.external.is_null() { + pub unsafe fn external_mut(&mut self) -> PluginResult> { + if unsafe { (*self.args_raw).external.is_null() } { return Ok(None); } - let external: *mut TaggedObject = self.args_raw.external as *mut TaggedObject; + let external: *mut TaggedObject = + unsafe { (*self.args_raw).external as *mut TaggedObject }; unsafe { if (*external).type_id != TypeId::of::() { @@ -525,9 +565,12 @@ impl<'a> OnBeforeParse<'a> { /// /// On Windows, this function may return an `Err(Error::Utf8(...))` if the /// source code contains invalid UTF-8. - pub fn input_source_code(&self) -> Result> { + pub fn input_source_code(&self) -> PluginResult> { let fetch_result = unsafe { - ((*self.result_raw).fetchSourceCode.unwrap())(self.args_raw, self.result_raw) + ((*self.result_raw).fetchSourceCode.unwrap())( + self.args_raw as *const _, + self.result_raw, + ) }; if fetch_result != 0 { @@ -587,7 +630,7 @@ impl<'a> OnBeforeParse<'a> { } /// Set the output loader for the current file. - pub fn set_output_loader(&self, loader: BunLogLevel) { + pub fn set_output_loader(&self, loader: BunLoader) { // SAFETY: We don't hand out mutable references to `result_raw` so dereferencing it is safe. unsafe { (*self.result_raw).loader = loader as u8; @@ -606,22 +649,36 @@ impl<'a> OnBeforeParse<'a> { /// Log a message with the given level. pub fn log(&self, message: &str, level: BunLogLevel) { - let mut log_options = sys::BunLogOptions { - __struct_size: std::mem::size_of::(), - message_ptr: message.as_ptr(), - message_len: message.len(), - path_ptr: self.args_raw.path_ptr, - path_len: self.args_raw.path_len, - source_line_text_ptr: std::ptr::null(), - source_line_text_len: 0, - level: level as i8, - line: 0, - lineEnd: 0, - column: 0, - columnEnd: 0, - }; + let mut log_options = log_from_message_and_level( + message, + level, + unsafe { (*self.args_raw).path_ptr }, + unsafe { (*self.args_raw).path_len }, + ); unsafe { ((*self.result_raw).log.unwrap())(self.args_raw, &mut log_options); } } } + +pub fn log_from_message_and_level( + message: &str, + level: BunLogLevel, + path: *const u8, + path_len: usize, +) -> sys::BunLogOptions { + sys::BunLogOptions { + __struct_size: std::mem::size_of::(), + message_ptr: message.as_ptr(), + message_len: message.len(), + path_ptr: path as *const _, + path_len, + source_line_text_ptr: std::ptr::null(), + source_line_text_len: 0, + level: level as i8, + line: 0, + lineEnd: 0, + column: 0, + columnEnd: 0, + } +} diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index cf55e532d962e2..0c2543b161c8fd 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -304,13 +304,21 @@ static LIBUS_SOCKET_DESCRIPTOR win32_set_nonblocking(LIBUS_SOCKET_DESCRIPTOR fd) } LIBUS_SOCKET_DESCRIPTOR bsd_set_nonblocking(LIBUS_SOCKET_DESCRIPTOR fd) { -#ifdef _WIN32 - /* Libuv will set windows sockets as non-blocking */ -#elif defined(__APPLE__) - fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK | O_CLOEXEC); -#else - fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); +/* Libuv will set windows sockets as non-blocking */ +#ifndef _WIN32 + if (LIKELY(fd != LIBUS_SOCKET_ERROR)) { + int flags = fcntl(fd, F_GETFL, 0); + + // F_GETFL supports O_NONBLCOK + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + flags = fcntl(fd, F_GETFD, 0); + + // F_GETFD supports FD_CLOEXEC + fcntl(fd, F_SETFD, flags | FD_CLOEXEC); + } #endif + return fd; } @@ -395,12 +403,27 @@ void bsd_socket_flush(LIBUS_SOCKET_DESCRIPTOR fd) { } LIBUS_SOCKET_DESCRIPTOR bsd_create_socket(int domain, int type, int protocol) { + LIBUS_SOCKET_DESCRIPTOR created_fd; #if defined(SOCK_CLOEXEC) && defined(SOCK_NONBLOCK) - int flags = SOCK_CLOEXEC | SOCK_NONBLOCK; - LIBUS_SOCKET_DESCRIPTOR created_fd = socket(domain, type | flags, protocol); + const int flags = SOCK_CLOEXEC | SOCK_NONBLOCK; + do { + created_fd = socket(domain, type | flags, protocol); + } while (IS_EINTR(created_fd)); + + if (UNLIKELY(created_fd == -1)) { + return LIBUS_SOCKET_ERROR; + } + return apple_no_sigpipe(created_fd); #else - LIBUS_SOCKET_DESCRIPTOR created_fd = socket(domain, type, protocol); + do { + created_fd = socket(domain, type, protocol); + } while (IS_EINTR(created_fd)); + + if (UNLIKELY(created_fd == -1)) { + return LIBUS_SOCKET_ERROR; + } + return bsd_set_nonblocking(apple_no_sigpipe(created_fd)); #endif } diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index efbf80dca9c8b8..17c27bdc144610 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -26,7 +26,7 @@ import { getBuildUrl, getEnv, getFileUrl, - getLoggedInUserCount, + getLoggedInUserCountOrDetails, getShell, getWindowsExitReason, isBuildkite, @@ -1499,7 +1499,7 @@ export async function main() { let waitForUser = false; while (isCI) { - const userCount = getLoggedInUserCount(); + const userCount = getLoggedInUserCountOrDetails(); if (!userCount) { if (waitForUser) { !isQuiet && console.log("No users logged in, exiting runner..."); @@ -1509,7 +1509,11 @@ export async function main() { if (!waitForUser) { startGroup("Summary"); - console.warn(`Found ${userCount} users logged in, keeping the runner alive until logout...`); + if (typeof userCount === "number") { + console.warn(`Found ${userCount} users logged in, keeping the runner alive until logout...`); + } else { + console.warn(userCount); + } waitForUser = true; } diff --git a/scripts/utils.mjs b/scripts/utils.mjs index be3dcfc69d8057..b391dc2c2d9856 100755 --- a/scripts/utils.mjs +++ b/scripts/utils.mjs @@ -2688,7 +2688,7 @@ export function printEnvironment() { /** * @returns {number | undefined} */ -export function getLoggedInUserCount() { +export function getLoggedInUserCountOrDetails() { if (isWindows) { const pwsh = which(["pwsh", "powershell"]); if (pwsh) { @@ -2705,7 +2705,31 @@ export function getLoggedInUserCount() { const { error, stdout } = spawnSync(["who"]); if (!error) { - return stdout.split("\n").filter(line => /tty|pts/i.test(line)).length; + const users = stdout + .split("\n") + .filter(line => /tty|pts/i.test(line)) + .map(line => { + // who output format: username terminal date/time (ip) + const [username, terminal, datetime, ip] = line.split(/\s+/); + return { + username, + terminal, + datetime, + ip: (ip || "").replace(/[()]/g, ""), // Remove parentheses from IP + }; + }); + + if (users.length === 0) { + return 0; + } + + let message = users.length + " currently logged in users:"; + + for (const user of users) { + message += `\n- ${user.username} on ${user.terminal} since ${user.datetime}${user.ip ? ` from ${user.ip}` : ""}`; + } + + return message; } } diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index e3daa4da1713db..6e05878e0ea5b9 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -439,6 +439,11 @@ pub const StandaloneModuleGraph = struct { const cleanup = struct { pub fn toClean(name: [:0]const u8, fd: bun.FileDescriptor) void { + // Ensure we own the file + if (Environment.isPosix) { + // Make the file writable so we can delete it + _ = Syscall.fchmod(fd, 0o777); + } _ = Syscall.close(fd); _ = Syscall.unlink(name); } diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index fe982a202dd371..036a12b8003775 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -969,6 +969,7 @@ pub const PosixSpawnOptions = struct { windows: void = {}, argv0: ?[*:0]const u8 = null, stream: bool = true, + sync: bool = false, /// Apple Extension: If this bit is set, rather /// than returning to the caller, posix_spawn(2) @@ -998,6 +999,7 @@ pub const WindowsSpawnResult = struct { stderr: StdioResult = .unavailable, extra_pipes: std.ArrayList(StdioResult) = std.ArrayList(StdioResult).init(bun.default_allocator), stream: bool = true, + sync: bool = false, pub const StdioResult = union(enum) { /// inherit, ignore, path, pipe @@ -1240,8 +1242,7 @@ pub fn spawnProcessPosix( var to_set_cloexec = std.ArrayList(bun.FileDescriptor).init(allocator); defer { for (to_set_cloexec.items) |fd| { - const fcntl_flags = bun.sys.fcntl(fd, std.posix.F.GETFD, 0).unwrap() catch continue; - _ = bun.sys.fcntl(fd, std.posix.F.SETFD, bun.C.FD_CLOEXEC | fcntl_flags); + _ = bun.sys.setCloseOnExec(fd); } to_set_cloexec.clearAndFree(); @@ -1323,24 +1324,9 @@ pub fn spawnProcessPosix( } const fds: [2]bun.FileDescriptor = brk: { - var fds_: [2]std.c.fd_t = undefined; - const rc = std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &fds_); - if (rc != 0) { - return error.SystemResources; - } - - { - const before = std.c.fcntl(fds_[if (i == 0) 1 else 0], std.posix.F.GETFD); - _ = std.c.fcntl(fds_[if (i == 0) 1 else 0], std.posix.F.SETFD, before | std.posix.FD_CLOEXEC); - } + const pair = try bun.sys.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, .blocking).unwrap(); - if (comptime Environment.isMac) { - // SO_NOSIGPIPE - const before: i32 = 1; - _ = std.c.setsockopt(fds_[if (i == 0) 1 else 0], std.posix.SOL.SOCKET, std.posix.SO.NOSIGPIPE, &before, @sizeOf(c_int)); - } - - break :brk .{ bun.toFD(fds_[if (i == 0) 1 else 0]), bun.toFD(fds_[if (i == 0) 0 else 1]) }; + break :brk .{ bun.toFD(pair[if (i == 0) 1 else 0]), bun.toFD(pair[if (i == 0) 0 else 1]) }; }; if (i == 0) { @@ -1381,6 +1367,10 @@ pub fn spawnProcessPosix( try to_close_at_end.append(fds[1]); try to_close_on_error.append(fds[0]); + if (!options.sync) { + try bun.sys.setNonblocking(fds[0]).unwrap(); + } + try actions.dup2(fds[1], fileno); if (fds[1] != fileno) try actions.close(fds[1]); @@ -1414,25 +1404,15 @@ pub fn spawnProcessPosix( try actions.open(fileno, path, bun.O.RDWR | bun.O.CREAT, 0o664); }, .ipc, .buffer => { - const fds: [2]bun.FileDescriptor = brk: { - var fds_: [2]std.c.fd_t = undefined; - const rc = std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &fds_); - if (rc != 0) { - return error.SystemResources; - } - - // enable non-block - var before = std.c.fcntl(fds_[0], std.posix.F.GETFD); - - _ = std.c.fcntl(fds_[0], std.posix.F.SETFD, before | bun.C.FD_CLOEXEC); - - if (comptime Environment.isMac) { - // SO_NOSIGPIPE - _ = std.c.setsockopt(fds_[if (i == 0) 1 else 0], std.posix.SOL.SOCKET, std.posix.SO.NOSIGPIPE, &before, @sizeOf(c_int)); - } - - break :brk .{ bun.toFD(fds_[0]), bun.toFD(fds_[1]) }; - }; + const fds: [2]bun.FileDescriptor = try bun.sys.socketpair( + std.posix.AF.UNIX, + std.posix.SOCK.STREAM, + 0, + if (ipc == .ipc) .nonblocking else .blocking, + ).unwrap(); + + if (!options.sync and ipc == .buffer) + try bun.sys.setNonblocking(fds[0]).unwrap(); try to_close_at_end.append(fds[1]); try to_close_on_error.append(fds[0]); diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index d4c21b6768274f..62cc3fa6c1f55c 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -428,12 +428,23 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, EncodedJSValue exportsValue = JSC::JSValue::encode(exports); JSC::JSValue resultValue = JSValue::decode(napi_register_module_v1(globalObject, exportsValue)); - // TODO: think about the finalizer here - // currently we do not dealloc napi modules so we don't have to worry about it right now - auto* meta = new Bun::NapiModuleMeta(globalObject->m_pendingNapiModuleDlopenHandle); - Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, nullptr); - bool success = resultValue.getObject()->putDirect(vm, WebCore::builtinNames(vm).napiDlopenHandlePrivateName(), napi_external, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); - ASSERT(success); + if (auto resultObject = resultValue.getObject()) { +#if OS(DARWIN) || OS(LINUX) + // If this is a native bundler plugin we want to store the handle from dlopen + // as we are going to call `dlsym()` on it later to get the plugin implementation. + const char** pointer_to_plugin_name = (const char**)dlsym(handle, "BUN_PLUGIN_NAME"); +#elif OS(WINDOWS) + const char** pointer_to_plugin_name = (const char**)GetProcAddress(handle, "BUN_PLUGIN_NAME"); +#endif + if (pointer_to_plugin_name) { + // TODO: think about the finalizer here + // currently we do not dealloc napi modules so we don't have to worry about it right now + auto* meta = new Bun::NapiModuleMeta(globalObject->m_pendingNapiModuleDlopenHandle); + Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, nullptr); + bool success = resultObject->putDirect(vm, WebCore::builtinNames(vm).napiDlopenHandlePrivateName(), napi_external, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); + ASSERT(success); + } + } RETURN_IF_EXCEPTION(scope, {}); diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 2d9142401f7725..4f2d6eb32f22cf 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -47,6 +47,9 @@ static JSC::JSObject* createErrorPrototype(JSC::VM& vm, JSC::JSGlobalObject* glo case JSC::ErrorType::Error: prototype = JSC::constructEmptyObject(globalObject, globalObject->errorPrototype()); break; + case JSC::ErrorType::URIError: + prototype = JSC::constructEmptyObject(globalObject, globalObject->m_URIErrorStructure.prototype(globalObject)); + break; default: { RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("TODO: Add support for more error types"); break; @@ -299,16 +302,9 @@ WTF::String ERR_OUT_OF_RANGE(JSC::ThrowScope& scope, JSC::JSGlobalObject* global namespace ERR { -JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& val_arg_name, const WTF::String& val_expected_type, JSC::JSValue val_actual_value) +JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral arg_name, WTF::ASCIILiteral expected_type, JSC::JSValue val_actual_value) { - auto arg_name = val_arg_name.span8(); - ASSERT(WTF::charactersAreAllASCII(arg_name)); - auto arg_kind = String(arg_name).startsWith("options."_s) ? "property"_s : "argument"_s; - - auto expected_type = val_expected_type.span8(); - ASSERT(WTF::charactersAreAllASCII(expected_type)); - auto ty_first_char = expected_type[0]; auto ty_kind = ty_first_char >= 'A' && ty_first_char <= 'Z' ? "an instance of"_s : "of type"_s; @@ -319,15 +315,11 @@ JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalO throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, message)); return {}; } -JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue val_arg_name, const WTF::String& val_expected_type, JSC::JSValue val_actual_value) +JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue val_arg_name, WTF::ASCIILiteral expected_type, JSC::JSValue val_actual_value) { auto arg_name = val_arg_name.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); - - auto arg_kind = String(arg_name).startsWith("options."_s) ? "property"_s : "argument"_s; - - auto expected_type = val_expected_type.span8(); - ASSERT(WTF::charactersAreAllASCII(expected_type)); + auto arg_kind = arg_name.startsWith("options."_s) ? "property"_s : "argument"_s; auto ty_first_char = expected_type[0]; auto ty_kind = ty_first_char >= 'A' && ty_first_char <= 'Z' ? "an instance of"_s : "of type"_s; @@ -405,15 +397,9 @@ JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObjec return {}; } -JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& name, JSC::JSValue value, const WTF::String& reason) +JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason) { - ASCIILiteral type; - { - auto sp = name.span8(); - auto str = std::string_view((const char*)(sp.data()), sp.size()); - auto has = str.find('.') == std::string::npos; - type = has ? "property"_s : "argument"_s; - } + ASCIILiteral type = String(name).find('.') != notFound ? "property"_s : "argument"_s; auto value_string = JSValueToStringSafe(globalObject, value); RETURN_IF_EXCEPTION(throwScope, {}); diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index 39c1d0f963373e..addcd371ba5828 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -75,14 +75,14 @@ enum Bound { namespace ERR { -JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& val_arg_name, const WTF::String& val_expected_type, JSC::JSValue val_actual_value); -JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue val_arg_name, const WTF::String& val_expected_type, JSC::JSValue val_actual_value); +JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::ASCIILiteral& arg_name, const WTF::ASCIILiteral& expected_type, JSC::JSValue val_actual_value); +JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name, WTF::ASCIILiteral expected_type, JSC::JSValue val_actual_value); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, size_t lower, size_t upper, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name, size_t lower, size_t upper, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, size_t bound_num, Bound bound, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, const WTF::String& msg, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name_val, const WTF::String& msg, JSC::JSValue actual); -JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); +JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& encoding); JSC::EncodedJSValue INVALID_STATE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& statemsg); diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 427f71fe198290..1bf4c70f14dbf9 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -47,6 +47,8 @@ export default [ ["ERR_UNKNOWN_SIGNAL", TypeError], ["ERR_SOCKET_BAD_PORT", RangeError], ["ERR_STREAM_RELEASE_LOCK", Error, "AbortError"], + ["ERR_INCOMPATIBLE_OPTION_PAIR", TypeError, "TypeError"], + ["ERR_INVALID_URI", URIError, "URIError"], // Bun-specific ["ERR_FORMDATA_PARSE_ERROR", TypeError], @@ -54,6 +56,9 @@ export default [ ["ERR_STREAM_WRAP", Error], ["ERR_BORINGSSL", Error], + // Console + ["ERR_CONSOLE_WRITABLE_STREAM", TypeError, "TypeError"], + //NET ["ERR_SOCKET_CLOSED_BEFORE_CONNECTION", Error], ["ERR_SOCKET_CLOSED", Error], diff --git a/src/bun.js/bindings/JSBundlerPlugin.cpp b/src/bun.js/bindings/JSBundlerPlugin.cpp index c1ec1fd4b8e393..80b25b4373f44d 100644 --- a/src/bun.js/bindings/JSBundlerPlugin.cpp +++ b/src/bun.js/bindings/JSBundlerPlugin.cpp @@ -38,6 +38,7 @@ namespace Bun { extern "C" int OnBeforeParsePlugin__isDone(void* context); +extern "C" void OnBeforeParseResult__reset(OnBeforeParseResult* result); #define WRAP_BUNDLER_PLUGIN(argName) jsDoubleNumber(bitwise_cast(reinterpret_cast(argName))) #define UNWRAP_BUNDLER_PLUGIN(callFrame) reinterpret_cast(bitwise_cast(callFrame->argument(0).asDouble())) @@ -61,21 +62,18 @@ void BundlerPlugin::NamespaceList::append(JSC::VM& vm, JSC::RegExp* filter, Stri if (nsGroup == nullptr) { namespaces.append(namespaceString); - groups.append(Vector {}); + groups.append(Vector {}); nsGroup = &groups.last(); index = namespaces.size() - 1; } - Yarr::RegularExpression regex( - StringView(filter->pattern()), - filter->flags()); - - nsGroup->append(WTFMove(regex)); + auto pattern = filter->pattern(); + auto filter_regexp = FilterRegExp(pattern, filter->flags()); + nsGroup->append(WTFMove(filter_regexp)); } static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& list, const BunString* namespaceStr, const BunString* path) { - constexpr bool usesPatternContextBuffer = false; if (list.fileNamespace.isEmpty() && list.namespaces.isEmpty()) return false; @@ -92,8 +90,7 @@ static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& li auto pathString = path->toWTFString(BunString::ZeroCopy); for (auto& filter : filters) { - Yarr::MatchingContextHolder regExpContext(vm, usesPatternContextBuffer, nullptr, Yarr::MatchFrom::CompilerThread); - if (filter.match(pathString) > -1) { + if (filter.match(vm, pathString)) { return true; } } @@ -243,18 +240,14 @@ void BundlerPlugin::NativePluginList::append(JSC::VM& vm, JSC::RegExp* filter, S if (nsGroup == nullptr) { namespaces.append(namespaceString); - groups.append(Vector {}); + groups.append(Vector {}); nsGroup = &groups.last(); index = namespaces.size() - 1; } - Yarr::RegularExpression regex( - StringView(filter->pattern()), - filter->flags()); - - NativeFilterRegexp nativeFilterRegexp = std::make_pair(regex, std::make_shared()); - - nsGroup->append(nativeFilterRegexp); + auto pattern = filter->pattern(); + auto filter_regexp = FilterRegExp(pattern, filter->flags()); + nsGroup->append(WTFMove(filter_regexp)); } if (index == std::numeric_limits::max()) { @@ -271,45 +264,54 @@ void BundlerPlugin::NativePluginList::append(JSC::VM& vm, JSC::RegExp* filter, S } } +bool BundlerPlugin::FilterRegExp::match(JSC::VM& vm, const String& path) +{ + WTF::Locker locker { lock }; + constexpr bool usesPatternContextBuffer = false; + Yarr::MatchingContextHolder regExpContext(vm, usesPatternContextBuffer, nullptr, Yarr::MatchFrom::CompilerThread); + return regex.match(path) != -1; +} + extern "C" void CrashHandler__setInsideNativePlugin(const char* plugin_name); -int BundlerPlugin::NativePluginList::call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, void* onBeforeParseArgs, void* onBeforeParseResult) +int BundlerPlugin::NativePluginList::call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, OnBeforeParseArguments* onBeforeParseArgs, OnBeforeParseResult* onBeforeParseResult) { unsigned index = 0; - const auto* group = this->group(namespaceStr->toWTFString(BunString::ZeroCopy), index); - if (group == nullptr) { + auto* groupPtr = this->group(namespaceStr->toWTFString(BunString::ZeroCopy), index); + if (groupPtr == nullptr) { return -1; } + auto& filters = *groupPtr; const auto& callbacks = index == std::numeric_limits::max() ? this->fileCallbacks : this->namespaceCallbacks[index]; - ASSERT_WITH_MESSAGE(callbacks.size() == group->size(), "Number of callbacks and filters must match"); + ASSERT_WITH_MESSAGE(callbacks.size() == filters.size(), "Number of callbacks and filters must match"); if (callbacks.isEmpty()) { return -1; } int count = 0; - constexpr bool usesPatternContextBuffer = false; const WTF::String& path = pathString->toWTFString(BunString::ZeroCopy); for (size_t i = 0, total = callbacks.size(); i < total && *shouldContinue; ++i) { - Yarr::MatchingContextHolder regExpContext(vm, usesPatternContextBuffer, nullptr, Yarr::MatchFrom::CompilerThread); - - // Need to lock the mutex to access the regular expression - { - std::lock_guard lock(*group->at(i).second); - if (group->at(i).first.match(path) > -1) { - Bun::NapiExternal* external = callbacks[i].external; - if (external) { - ((OnBeforeParseArguments*)(onBeforeParseArgs))->external = external->value(); - } - - JSBundlerPluginNativeOnBeforeParseCallback callback = callbacks[i].callback; - const char* name = callbacks[i].name ? callbacks[i].name : ""; - CrashHandler__setInsideNativePlugin(name); - callback(onBeforeParseArgs, onBeforeParseResult); - CrashHandler__setInsideNativePlugin(nullptr); - - count++; + + if (i > 0) { + OnBeforeParseResult__reset(onBeforeParseResult); + } + + if (filters[i].match(vm, path)) { + Bun::NapiExternal* external = callbacks[i].external; + if (external) { + onBeforeParseArgs->external = external->value(); + } else { + onBeforeParseArgs->external = nullptr; } + + JSBundlerPluginNativeOnBeforeParseCallback callback = callbacks[i].callback; + const char* name = callbacks[i].name ? callbacks[i].name : ""; + CrashHandler__setInsideNativePlugin(name); + callback(onBeforeParseArgs, onBeforeParseResult); + CrashHandler__setInsideNativePlugin(nullptr); + + count++; } if (OnBeforeParsePlugin__isDone(bunContextPtr)) { @@ -373,7 +375,7 @@ JSC_DEFINE_HOST_FUNCTION(jsBundlerPluginFunction_onBeforeParse, (JSC::JSGlobalOb #endif if (!on_before_parse_symbol_ptr) { - Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "Expected on_before_parse_symbol (3rd argument) to be a valid symbol"_s); + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, makeString("Could not find the symbol \""_s, on_before_parse_symbol, "\" in the given napi module."_s)); return {}; } @@ -648,7 +650,7 @@ extern "C" int JSBundlerPlugin__callOnBeforeParsePlugins( const BunString* namespaceStr, const BunString* pathString, OnBeforeParseArguments* onBeforeParseArgs, - void* onBeforeParseResult, + OnBeforeParseResult* onBeforeParseResult, int* shouldContinue) { return plugin->plugin.onBeforeParse.call(plugin->vm(), &plugin->plugin, shouldContinue, bunContextPtr, namespaceStr, pathString, onBeforeParseArgs, onBeforeParseResult); diff --git a/src/bun.js/bindings/JSBundlerPlugin.h b/src/bun.js/bindings/JSBundlerPlugin.h index da28a8e4857335..7bef5769fa2626 100644 --- a/src/bun.js/bindings/JSBundlerPlugin.h +++ b/src/bun.js/bindings/JSBundlerPlugin.h @@ -1,5 +1,6 @@ #pragma once +#include "bun-native-bundler-plugin-api/bundler_plugin.h" #include "root.h" #include "headers-handwritten.h" #include @@ -10,7 +11,7 @@ typedef void (*JSBundlerPluginAddErrorCallback)(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue); typedef void (*JSBundlerPluginOnLoadAsyncCallback)(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue); typedef void (*JSBundlerPluginOnResolveAsyncCallback)(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue, JSC::EncodedJSValue); -typedef void (*JSBundlerPluginNativeOnBeforeParseCallback)(void*, void*); +typedef void (*JSBundlerPluginNativeOnBeforeParseCallback)(const OnBeforeParseArguments*, OnBeforeParseResult*); namespace Bun { @@ -18,14 +19,38 @@ using namespace JSC; class BundlerPlugin final { public: + /// In native plugins, the regular expression could be called concurrently on multiple threads. + /// Therefore, we need a mutex to synchronize access. + class FilterRegExp { + public: + String m_pattern; + Yarr::RegularExpression regex; + WTF::Lock lock {}; + + FilterRegExp(FilterRegExp&& other) + : m_pattern(WTFMove(other.m_pattern)) + , regex(WTFMove(other.regex)) + { + } + + FilterRegExp(const String& pattern, OptionSet flags) + // Ensure it's safe for cross-thread usage. + : m_pattern(pattern.isolatedCopy()) + , regex(m_pattern, flags) + { + } + + bool match(JSC::VM& vm, const String& path); + }; + class NamespaceList { public: - Vector fileNamespace = {}; + Vector fileNamespace = {}; Vector namespaces = {}; - Vector> groups = {}; + Vector> groups = {}; BunPluginTarget target { BunPluginTargetBun }; - Vector* group(const String& namespaceStr, unsigned& index) + Vector* group(const String& namespaceStr, unsigned& index) { if (namespaceStr.isEmpty()) { index = std::numeric_limits::max(); @@ -46,10 +71,6 @@ class BundlerPlugin final { void append(JSC::VM& vm, JSC::RegExp* filter, String& namespaceString, unsigned& index); }; - /// In native plugins, the regular expression could be called concurrently on multiple threads. - /// Therefore, we need a mutex to synchronize access. - typedef std::pair> NativeFilterRegexp; - struct NativePluginCallback { JSBundlerPluginNativeOnBeforeParseCallback callback; Bun::NapiExternal* external; @@ -65,18 +86,18 @@ class BundlerPlugin final { public: using PerNamespaceCallbackList = Vector; - Vector fileNamespace = {}; + Vector fileNamespace = {}; Vector namespaces = {}; - Vector> groups = {}; + Vector> groups = {}; BunPluginTarget target { BunPluginTargetBun }; PerNamespaceCallbackList fileCallbacks = {}; Vector namespaceCallbacks = {}; - int call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, void* onBeforeParseArgs, void* onBeforeParseResult); + int call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, OnBeforeParseArguments* onBeforeParseArgs, OnBeforeParseResult* onBeforeParseResult); void append(JSC::VM& vm, JSC::RegExp* filter, String& namespaceString, JSBundlerPluginNativeOnBeforeParseCallback callback, const char* name, NapiExternal* external); - Vector* group(const String& namespaceStr, unsigned& index) + Vector* group(const String& namespaceStr, unsigned& index) { if (namespaceStr.isEmpty()) { index = std::numeric_limits::max(); diff --git a/src/bun.js/bindings/JSStringDecoder.cpp b/src/bun.js/bindings/JSStringDecoder.cpp index 95200ce646fd57..ff6a0c18c06994 100644 --- a/src/bun.js/bindings/JSStringDecoder.cpp +++ b/src/bun.js/bindings/JSStringDecoder.cpp @@ -2,6 +2,7 @@ #include "JSBuffer.h" #include #include +#include "JavaScriptCore/ExceptionScope.h" #include "ZigGlobalObject.h" #include "JSDOMOperation.h" #include "JSDOMAttribute.h" @@ -9,8 +10,11 @@ #include "JSDOMConvertEnumeration.h" #include #include "BunClientData.h" +#include "wtf/text/ASCIILiteral.h" #include "wtf/text/StringImpl.h" #include "wtf/unicode/CharacterNames.h" +#include "wtf/SIMDUTF.h" +#include "ErrorCode.h" namespace WebCore { @@ -23,104 +27,85 @@ static JSC_DECLARE_HOST_FUNCTION(jsStringDecoderPrototypeFunction_text); static JSC_DECLARE_CUSTOM_GETTER(jsStringDecoder_lastChar); static JSC_DECLARE_CUSTOM_GETTER(jsStringDecoder_lastNeed); static JSC_DECLARE_CUSTOM_GETTER(jsStringDecoder_lastTotal); +static JSC_DECLARE_CUSTOM_GETTER(jsStringDecoder_encoding); static WTF::String replacementString() { - return WTF::String(std::span { u"\uFFFD", 1 }); } - -static inline JSC::EncodedJSValue jsStringDecoderCast(JSGlobalObject* globalObject, JSValue stringDecoderValue) +static WTF::String replacementString2() { - if (LIKELY(jsDynamicCast(stringDecoderValue))) - return JSValue::encode(stringDecoderValue); - - auto& vm = globalObject->vm(); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - if (stringDecoderValue.isEmpty() || stringDecoderValue.isUndefinedOrNull()) { - return JSC::JSValue::encode(jsUndefined()); - } - - if (!stringDecoderValue.isObject()) { - return throwThisTypeError(*globalObject, throwScope, JSStringDecoder::info()->className, "write"); - } - - JSC::JSObject* thisObject = JSC::asObject(stringDecoderValue); - JSStringDecoder* castedThis = nullptr; - auto clientData = WebCore::clientData(vm); - if (JSValue existingDecoderValue = thisObject->getIfPropertyExists(globalObject, clientData->builtinNames().decodePrivateName())) { - castedThis = jsDynamicCast(existingDecoderValue); - } - - if (!castedThis) { - BufferEncodingType encoding = BufferEncodingType::utf8; - if (JSValue encodingValue = thisObject->getIfPropertyExists(globalObject, clientData->builtinNames().encodingPrivateName())) { - if (encodingValue.isString()) { - std::optional opt = parseEnumeration(*globalObject, encodingValue); - if (opt.has_value()) { - encoding = opt.value(); - } - } - } - castedThis = JSStringDecoder::create(globalObject->vm(), globalObject, reinterpret_cast(globalObject)->JSStringDecoderStructure(), encoding); - thisObject->putDirect(vm, clientData->builtinNames().decodePrivateName(), castedThis, 0); - } + return WTF::String(std::span { u"\uFFFD\uFFFD", 2 }); +} +static WTF::String replacementString3() +{ + return WTF::String(std::span { u"\uFFFD\uFFFD\uFFFD", 3 }); +} - return JSValue::encode(castedThis); +// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a +// continuation byte. +// 0 1 2 3 4 5 6 7 8 9 A B C D E F +// 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +// 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +// 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +// 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +// 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +// 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +// 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +// 7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +// 8 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 +// 9 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 +// A -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 +// B -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 +// C 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 +// D 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 +// E 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +// F 4 4 4 4 4 4 4 4 -2 -2 -2 -2 -2 -2 -2 -2 +int8_t utf8CheckByte(uint8_t byte) +{ + if (byte <= 0x7F) + return 0; // ASCII + else if ((byte >> 5) == 0x06) + return 2; // 2-byte Start + else if ((byte >> 4) == 0x0E) + return 3; // 3-byte Start + else if ((byte >> 3) == 0x1E) + return 4; // 4-byte Start + return (byte >> 6) == 0x02 + ? -1 // Continuation + : -2; // Invalid } -void JSStringDecoder::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +ALWAYS_INLINE bool isContinuation(uint8_t byte) { - Base::finishCreation(vm); + return (byte & 0xC0) == 0x80; } -JSC::JSValue JSStringDecoder::fillLast(JSC::VM& vm, JSC::JSGlobalObject* globalObject, uint8_t* bufPtr, uint32_t length) +static inline JSStringDecoder* jsStringDecoderCast(JSGlobalObject* globalObject, JSValue stringDecoderValue, WTF::ASCIILiteral functionName) { + ASSERT(stringDecoderValue); + if (auto cast = jsDynamicCast(stringDecoderValue); LIKELY(cast)) + return cast; + + auto& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); - if (m_encoding == BufferEncodingType::utf8) { - // utf8CheckExtraBytes - if ((bufPtr[0] & 0xC0) != 0x80) { - m_lastNeed = 0; - RELEASE_AND_RETURN(throwScope, JSC::jsString(vm, replacementString())); - } - if (m_lastNeed > 1 && length > 1) { - if ((bufPtr[1] & 0xC0) != 0x80) { - m_lastNeed = 1; - RELEASE_AND_RETURN(throwScope, JSC::jsString(vm, replacementString())); - } - if (m_lastNeed > 2 && length > 2) { - if ((bufPtr[2] & 0xC0) != 0x80) { - m_lastNeed = 2; - RELEASE_AND_RETURN(throwScope, JSC::jsString(vm, replacementString())); - } - } + if (JSC::JSObject* thisObject = stringDecoderValue.getObject()) { + auto clientData = WebCore::clientData(vm); + JSValue existingDecoderValue = thisObject->getIfPropertyExists(globalObject, clientData->builtinNames().decodePrivateName()); + if (LIKELY(existingDecoderValue)) { + if (auto cast = jsDynamicCast(existingDecoderValue); LIKELY(cast)) + return cast; } } - if (m_lastNeed <= length) { - memmove(m_lastChar + m_lastTotal - m_lastNeed, bufPtr, m_lastNeed); - RELEASE_AND_RETURN(throwScope, JSC::JSValue::decode(Bun__encoding__toString(m_lastChar, m_lastTotal, globalObject, static_cast(m_encoding)))); - } - memmove(m_lastChar + m_lastTotal - m_lastNeed, bufPtr, length); - m_lastNeed -= length; - RELEASE_AND_RETURN(throwScope, JSC::jsEmptyString(vm)); + throwThisTypeError(*globalObject, throwScope, JSStringDecoder::info()->className, functionName); + return nullptr; } -// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a -// continuation byte. If an invalid byte is detected, -2 is returned. -int8_t utf8CheckByte(uint8_t byte) +void JSStringDecoder::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) { - if (byte <= 0x7F) - return 0; - else if ((byte >> 5) == 0x06) - return 2; - else if ((byte >> 4) == 0x0E) - return 3; - else if ((byte >> 3) == 0x1E) - return 4; - return (byte >> 6) == 0x02 ? -1 : -2; + Base::finishCreation(vm); } // Checks at most 3 bytes at the end of a Buffer in order to detect an @@ -160,6 +145,60 @@ uint8_t JSStringDecoder::utf8CheckIncomplete(uint8_t* bufPtr, uint32_t length, u return 0; } +JSC::JSValue JSStringDecoder::fillLast(JSC::VM& vm, JSC::JSGlobalObject* globalObject, uint8_t* bufPtr, uint32_t length) +{ + auto throwScope = DECLARE_THROW_SCOPE(vm); + + if (m_encoding == BufferEncodingType::utf8) { + // Check if the start has a failing UTF-8 code point. This is checking + // for situations where the a new character starts instead of a + // continuation byte. In this case, lastNeed (offset for decoding the + // rest of bufPtr) needs to be set less than the number of codepoints + // from what lastChar[0] requests since a new character starts. + // Example: + // [ 0xcc ] + [ 0xcc, 0x8c ] + // The first byte is not known to be an error until the second chunk + // comes in, to which the error is just the first 0xcc, and then + // the second two bytes are seen as the valid code point. + uint32_t max = std::min(length, m_lastNeed); + for (uint32_t i = 0; i < max; i++) { + if (!isContinuation(bufPtr[i])) { + // copy the continuation bytes to lastChar, then run it through + // originally this had an abridged version of the utf-8 decoder, + // but doing that is going to be more error prone. + // Example: [ 0xf2, 0x90 ] + [ 0xD0 ] -> '' + '\uFFFD' + '\uFFFD' + // ~~~~~~~~~~ ~~~~ two total errors + // Example: [ 0xf6, 0x90 ] + [ 0xD0 ] -> '' + '\uFFFD\uFFFD' + '\uFFFD' + // ~~~~ ~~~~ ~~~~ three total errors + // ^ 0xF6 is an invalid start byte + uint32_t chars = m_lastTotal - m_lastNeed + i; + memmove(m_lastChar + m_lastTotal - m_lastNeed, bufPtr, i); + m_lastNeed = i; + RELEASE_AND_RETURN(throwScope, JSC::JSValue::decode(Bun__encoding__toString(m_lastChar, chars, globalObject, static_cast(m_encoding)))); + } + } + } + if (m_lastNeed <= length) { + memmove(m_lastChar + m_lastTotal - m_lastNeed, bufPtr, m_lastNeed); + RELEASE_AND_RETURN(throwScope, JSC::JSValue::decode(Bun__encoding__toString(m_lastChar, m_lastTotal, globalObject, static_cast(m_encoding)))); + } + + memmove(m_lastChar + m_lastTotal - m_lastNeed, bufPtr, length); + if (m_encoding == BufferEncodingType::utf8) { + uint32_t lastLastNeed = m_lastNeed; + uint32_t total = utf8CheckIncomplete(m_lastChar, m_lastTotal - lastLastNeed + length, 0); + if (total == 0) { + uint32_t len = m_lastTotal - m_lastNeed + length; + m_lastNeed = length; + RELEASE_AND_RETURN(throwScope, JSC::JSValue::decode(Bun__encoding__toString(m_lastChar, len, globalObject, static_cast(m_encoding)))); + } + m_lastNeed = lastLastNeed; + } + + m_lastNeed -= length; + RELEASE_AND_RETURN(throwScope, JSC::jsEmptyString(vm)); +} + // This is not the exposed text JSC::JSValue JSStringDecoder::text(JSC::VM& vm, JSC::JSGlobalObject* globalObject, uint8_t* bufPtr, uint32_t length, uint32_t offset) { @@ -241,6 +280,8 @@ JSC::JSValue JSStringDecoder::write(JSC::VM& vm, JSC::JSGlobalObject* globalObje RELEASE_AND_RETURN(throwScope, firstHalf); offset = m_lastNeed; m_lastNeed = 0; + if (offset == length) + RELEASE_AND_RETURN(throwScope, firstHalf); JSString* secondHalf = text(vm, globalObject, bufPtr, length, offset).toString(globalObject); RETURN_IF_EXCEPTION(throwScope, JSC::jsUndefined()); @@ -305,11 +346,17 @@ JSStringDecoder::end(JSC::VM& vm, JSC::JSGlobalObject* globalObject, uint8_t* bu } case BufferEncodingType::utf8: { if (length == 0) { - RELEASE_AND_RETURN(throwScope, m_lastNeed ? JSC::jsString(vm, replacementString()) : JSC::jsEmptyString(vm)); + RELEASE_AND_RETURN(throwScope, m_lastNeed ? JSC::JSValue::decode(Bun__encoding__toString(m_lastChar, m_lastTotal - m_lastNeed, globalObject, static_cast(m_encoding))) : JSC::jsEmptyString(vm)); } JSString* firstHalf = write(vm, globalObject, bufPtr, length).toString(globalObject); RETURN_IF_EXCEPTION(throwScope, JSC::jsUndefined()); - RELEASE_AND_RETURN(throwScope, m_lastNeed ? JSC::jsString(globalObject, firstHalf, replacementString()) : firstHalf); + RELEASE_AND_RETURN(throwScope, + m_lastNeed + ? JSC::jsString( + globalObject, + firstHalf, + jsCast(JSC::JSValue::decode(Bun__encoding__toString(m_lastChar, m_lastTotal - m_lastNeed, globalObject, static_cast(m_encoding))))) + : firstHalf); } case BufferEncodingType::base64: case BufferEncodingType::base64url: { @@ -373,8 +420,7 @@ static inline JSC::EncodedJSValue jsStringDecoderPrototypeFunction_writeBody(JSC return JSC::JSValue::encode(buffer); } - throwVMTypeError(lexicalGlobalObject, throwScope, "Expected Uint8Array"_s); - return {}; + return Bun::ERR::INVALID_ARG_TYPE(throwScope, lexicalGlobalObject, "buf"_s, "Buffer, TypedArray, or DataView"_s, buffer); } RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(castedThis->write(vm, lexicalGlobalObject, reinterpret_cast(view->vector()), view->byteLength()))); } @@ -420,70 +466,63 @@ static inline JSC::EncodedJSValue jsStringDecoderPrototypeFunction_textBody(JSC: JSC_DEFINE_HOST_FUNCTION(jsStringDecoderPrototypeFunction_write, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - JSValue stringDecoderValue = JSValue::decode(jsStringDecoderCast(globalObject, callFrame->thisValue())); - if (stringDecoderValue.isEmpty() || !stringDecoderValue.isCell()) { - return JSValue::encode(stringDecoderValue); - } - JSStringDecoder* castedThis = jsCast(stringDecoderValue); + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSStringDecoder* castedThis = jsStringDecoderCast(globalObject, callFrame->thisValue(), "write"_s); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); return jsStringDecoderPrototypeFunction_writeBody(globalObject, callFrame, castedThis); } JSC_DEFINE_HOST_FUNCTION(jsStringDecoderPrototypeFunction_end, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - JSValue stringDecoderValue = JSValue::decode(jsStringDecoderCast(globalObject, callFrame->thisValue())); - if (stringDecoderValue.isEmpty() || !stringDecoderValue.isCell()) { - return JSValue::encode(stringDecoderValue); - } - JSStringDecoder* castedThis = jsCast(stringDecoderValue); + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSStringDecoder* castedThis = jsStringDecoderCast(globalObject, callFrame->thisValue(), "end"_s); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); return jsStringDecoderPrototypeFunction_endBody(globalObject, callFrame, castedThis); } JSC_DEFINE_HOST_FUNCTION(jsStringDecoderPrototypeFunction_text, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - JSValue stringDecoderValue = JSValue::decode(jsStringDecoderCast(globalObject, callFrame->thisValue())); - if (stringDecoderValue.isEmpty() || !stringDecoderValue.isCell()) { - return JSValue::encode(stringDecoderValue); - } - JSStringDecoder* castedThis = jsCast(stringDecoderValue); - + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSStringDecoder* castedThis = jsStringDecoderCast(globalObject, callFrame->thisValue(), "text"_s); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); return jsStringDecoderPrototypeFunction_textBody(globalObject, callFrame, castedThis); } static JSC_DEFINE_CUSTOM_GETTER(jsStringDecoder_lastChar, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = JSC::getVM(lexicalGlobalObject); - JSValue stringDecoderValue = JSValue::decode(jsStringDecoderCast(lexicalGlobalObject, JSValue::decode(thisValue))); - if (stringDecoderValue.isEmpty() || !stringDecoderValue.isCell()) { - return JSValue::encode(stringDecoderValue); - } - JSStringDecoder* thisObject = jsCast(stringDecoderValue); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto buffer = ArrayBuffer::create({ thisObject->m_lastChar, 4 }); + auto scope = DECLARE_THROW_SCOPE(vm); + JSStringDecoder* castedThis = jsStringDecoderCast(lexicalGlobalObject, JSC::JSValue::decode(thisValue), "text"_s); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); + auto buffer = ArrayBuffer::create({ castedThis->m_lastChar, 4 }); auto* globalObject = reinterpret_cast(lexicalGlobalObject); JSC::JSUint8Array* uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, globalObject->JSBufferSubclassStructure(), WTFMove(buffer), 0, 4); - RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(uint8Array)); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(uint8Array)); } static JSC_DEFINE_CUSTOM_GETTER(jsStringDecoder_lastNeed, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = JSC::getVM(lexicalGlobalObject); - JSValue stringDecoderValue = JSValue::decode(jsStringDecoderCast(lexicalGlobalObject, JSValue::decode(thisValue))); - if (stringDecoderValue.isEmpty() || !stringDecoderValue.isCell()) { - return JSValue::encode(stringDecoderValue); - } - JSStringDecoder* thisObject = jsCast(stringDecoderValue); - auto throwScope = DECLARE_THROW_SCOPE(vm); - RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(JSC::jsNumber(thisObject->m_lastNeed))); + auto scope = DECLARE_THROW_SCOPE(vm); + JSStringDecoder* castedThis = jsStringDecoderCast(lexicalGlobalObject, JSC::JSValue::decode(thisValue), "lastNeed"_s); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::jsNumber(castedThis->m_lastNeed))); } static JSC_DEFINE_CUSTOM_GETTER(jsStringDecoder_lastTotal, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = JSC::getVM(lexicalGlobalObject); - JSValue stringDecoderValue = JSValue::decode(jsStringDecoderCast(lexicalGlobalObject, JSValue::decode(thisValue))); - if (stringDecoderValue.isEmpty() || !stringDecoderValue.isCell()) { - return JSValue::encode(stringDecoderValue); - } - JSStringDecoder* thisObject = jsCast(stringDecoderValue); - auto throwScope = DECLARE_THROW_SCOPE(vm); - RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(JSC::jsNumber(thisObject->m_lastTotal))); + auto scope = DECLARE_THROW_SCOPE(vm); + JSStringDecoder* castedThis = jsStringDecoderCast(lexicalGlobalObject, JSC::JSValue::decode(thisValue), "lastTotal"_s); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::jsNumber(castedThis->m_lastTotal))); +} + +static JSC_DEFINE_CUSTOM_GETTER(jsStringDecoder_encoding, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSStringDecoder* castedThis = jsStringDecoderCast(lexicalGlobalObject, JSC::JSValue::decode(thisValue), "lastTotal"_s); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); + return JSC::JSValue::encode(WebCore::convertEnumerationToJS(*lexicalGlobalObject, castedThis->m_encoding)); } /* Hash table for prototype */ @@ -492,6 +531,7 @@ static const HashTableValue JSStringDecoderPrototypeTableValues[] { "lastChar"_s, static_cast(JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsStringDecoder_lastChar, 0 } }, { "lastNeed"_s, static_cast(JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsStringDecoder_lastNeed, 0 } }, { "lastTotal"_s, static_cast(JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsStringDecoder_lastTotal, 0 } }, + { "encoding"_s, static_cast(JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsStringDecoder_encoding, 0 } }, { "write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsStringDecoderPrototypeFunction_write, 1 } }, { "end"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsStringDecoderPrototypeFunction_end, 1 } }, { "text"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsStringDecoderPrototypeFunction_text, 2 } }, @@ -523,15 +563,17 @@ JSStringDecoderConstructor* JSStringDecoderConstructor::create(JSC::VM& vm, JSC: JSC::EncodedJSValue JSStringDecoderConstructor::construct(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) { JSC::VM& vm = lexicalGlobalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); auto encoding = BufferEncodingType::utf8; - if (callFrame->argumentCount() > 0) { - - auto encoding_ = callFrame->argument(0); - if (encoding_.isString()) { - std::optional opt = parseEnumeration(*lexicalGlobalObject, encoding_); - if (opt.has_value()) { - encoding = opt.value(); - } + auto jsEncoding = callFrame->argument(0); + if (!jsEncoding.isUndefinedOrNull()) { + std::optional opt = parseEnumeration(*lexicalGlobalObject, jsEncoding); + if (opt.has_value()) { + encoding = opt.value(); + } else { + WTF::String encodingString = jsEncoding.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + return Bun::ERR::UNKNOWN_ENCODING(throwScope, lexicalGlobalObject, encodingString); } } JSValue thisValue = callFrame->newTarget(); @@ -552,6 +594,7 @@ JSC::EncodedJSValue JSStringDecoderConstructor::construct(JSC::JSGlobalObject* l JSObject* thisObject = asObject(callFrame->thisValue()); thisObject->putDirect(vm, clientData->builtinNames().decodePrivateName(), jsObject, JSC::PropertyAttribute::DontEnum | 0); + thisObject->putDirect(vm, clientData->builtinNames().encodingPublicName(), convertEnumerationToJS(*lexicalGlobalObject, encoding), JSC::PropertyAttribute::DontEnum | 0); return JSC::JSValue::encode(thisObject); } diff --git a/src/bun.js/bindings/JSStringDecoder.h b/src/bun.js/bindings/JSStringDecoder.h index ebbfccf0aa5653..58f1b270f1f9cf 100644 --- a/src/bun.js/bindings/JSStringDecoder.h +++ b/src/bun.js/bindings/JSStringDecoder.h @@ -54,16 +54,15 @@ class JSStringDecoder : public JSC::JSDestructibleObject { JSC::JSValue write(JSC::VM&, JSC::JSGlobalObject*, uint8_t*, uint32_t); JSC::JSValue end(JSC::VM&, JSC::JSGlobalObject*, uint8_t*, uint32_t); - uint8_t m_lastNeed; - uint8_t m_lastTotal; - uint8_t m_lastChar[4]; + uint8_t m_lastNeed = 0; + uint8_t m_lastTotal = 0; + uint8_t m_lastChar[4] = { 0, 0, 0, 0 }; + BufferEncodingType m_encoding = BufferEncodingType::utf8; private: JSC::JSValue fillLast(JSC::VM&, JSC::JSGlobalObject*, uint8_t*, uint32_t); JSC::JSValue text(JSC::VM&, JSC::JSGlobalObject*, uint8_t*, uint32_t, uint32_t); uint8_t utf8CheckIncomplete(uint8_t*, uint32_t, uint32_t); - - BufferEncodingType m_encoding; }; class JSStringDecoderPrototype : public JSC::JSNonFinalObject { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 7305ad8930e137..8c5369b15aa1bb 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4199,7 +4199,9 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, BunStr // It is key to not clone this data because source code strings are large. // Usage of toStringView (non-owning) is safe as we ref the provider. provider->ref(); - ASSERT(*referenced_source_provider == nullptr); + if (*referenced_source_provider != nullptr) { + (*referenced_source_provider)->deref(); + } *referenced_source_provider = provider; source_lines[0] = Bun::toStringView(sourceString.substring(lineStart, lineEnd - lineStart)); source_line_numbers[0] = location.line(); diff --git a/src/bun.js/bindings/webcore/Event.cpp b/src/bun.js/bindings/webcore/Event.cpp index e68c829d92473a..a834a5e33697b3 100644 --- a/src/bun.js/bindings/webcore/Event.cpp +++ b/src/bun.js/bindings/webcore/Event.cpp @@ -137,11 +137,16 @@ void Event::setCurrentTarget(EventTarget* currentTarget, std::optional isI m_currentTargetIsInShadowTree = false; // m_currentTargetIsInShadowTree = isInShadowTree ? *isInShadowTree : (is(currentTarget) && downcast(*currentTarget).isInShadowTree()); } -Vector Event::composedPath() const +void Event::setEventPath(const EventPath& path) { - // if (!m_eventPath) - return Vector(); - // return m_eventPath->computePathUnclosedToTarget(*m_currentTarget); + m_eventPath = &path; +} + +Vector> Event::composedPath() const +{ + if (!m_eventPath) + return Vector>(); + return m_eventPath->computePathUnclosedToTarget(*m_currentTarget); } void Event::setUnderlyingEvent(Event* underlyingEvent) diff --git a/src/bun.js/bindings/webcore/Event.h b/src/bun.js/bindings/webcore/Event.h index 5b7d5cba2a88e6..748251f9b44f6d 100644 --- a/src/bun.js/bindings/webcore/Event.h +++ b/src/bun.js/bindings/webcore/Event.h @@ -91,8 +91,8 @@ class Event : public ScriptWrappable, public RefCounted { DOMHighResTimeStamp timeStampForBindings(ScriptExecutionContext&) const; MonotonicTime timeStamp() const { return m_createTime; } - void setEventPath(const EventPath& path) { UNUSED_PARAM(path); } - Vector composedPath() const; + void setEventPath(const EventPath&); + Vector> composedPath() const; void stopPropagation() { m_propagationStopped = true; } void stopImmediatePropagation() { m_immediatePropagationStopped = true; } diff --git a/src/bun.js/bindings/webcore/EventPath.cpp b/src/bun.js/bindings/webcore/EventPath.cpp index bb285566c332bb..5be68a187f073f 100644 --- a/src/bun.js/bindings/webcore/EventPath.cpp +++ b/src/bun.js/bindings/webcore/EventPath.cpp @@ -237,56 +237,62 @@ EventPath::EventPath(Node& originalTarget, Event& event) // #endif -// // https://dom.spec.whatwg.org/#dom-event-composedpath -// // Any node whose depth computed in EventPath::buildPath is greater than the context object is excluded. -// // Because we can exit out of a closed shadow tree and re-enter another closed shadow tree via a slot, -// // we decrease the *allowed depth* whenever we moved to a "shallower" (closer-to-document) tree. -// Vector EventPath::computePathUnclosedToTarget(const EventTarget& target) const -// { -// Vector path; -// auto pathSize = m_path.size(); -// RELEASE_ASSERT(pathSize); -// path.reserveInitialCapacity(pathSize); - -// auto currentTargetIndex = m_path.findIf([&target](auto& context) { -// return context.currentTarget() == ⌖ -// }); -// RELEASE_ASSERT(currentTargetIndex != notFound); -// auto currentTargetDepth = m_path[currentTargetIndex].closedShadowDepth(); - -// auto appendTargetWithLesserDepth = [&path](const EventContext& currentContext, int& currentDepthAllowed) { -// auto depth = currentContext.closedShadowDepth(); -// bool contextIsInsideInnerShadowTree = depth > currentDepthAllowed; -// if (contextIsInsideInnerShadowTree) -// return; -// bool movedOutOfShadowTree = depth < currentDepthAllowed; -// if (movedOutOfShadowTree) -// currentDepthAllowed = depth; -// path.unsafeAppendWithoutCapacityCheck(currentContext.currentTarget()); -// }; - -// auto currentDepthAllowed = currentTargetDepth; -// auto i = currentTargetIndex; -// do { -// appendTargetWithLesserDepth(m_path[i], currentDepthAllowed); -// } while (i--); -// path.reverse(); - -// currentDepthAllowed = currentTargetDepth; -// for (auto i = currentTargetIndex + 1; i < pathSize; ++i) -// appendTargetWithLesserDepth(m_path[i], currentDepthAllowed); - -// return path; -// } +// https://dom.spec.whatwg.org/#dom-event-composedpath +// Any node whose depth computed in EventPath::buildPath is greater than the context object is excluded. +// Because we can exit out of a closed shadow tree and re-enter another closed shadow tree via a slot, +// we decrease the *allowed depth* whenever we moved to a "shallower" (closer-to-document) tree. +Vector> EventPath::computePathUnclosedToTarget(const EventTarget& target) const +{ + Vector> path; + auto pathSize = m_path.size(); + RELEASE_ASSERT(pathSize); + path.reserveInitialCapacity(pathSize); + + auto currentTargetIndex = m_path.findIf([&target](auto& context) { + return context.currentTarget() == ⌖ + }); + RELEASE_ASSERT(currentTargetIndex != notFound); + auto currentTargetDepth = m_path[currentTargetIndex].closedShadowDepth(); + + auto appendTargetWithLesserDepth = [&path](const EventContext& currentContext, int& currentDepthAllowed) { + auto depth = currentContext.closedShadowDepth(); + bool contextIsInsideInnerShadowTree = depth > currentDepthAllowed; + if (contextIsInsideInnerShadowTree) + return; + bool movedOutOfShadowTree = depth < currentDepthAllowed; + if (movedOutOfShadowTree) + currentDepthAllowed = depth; + path.append(*currentContext.currentTarget()); + }; + + auto currentDepthAllowed = currentTargetDepth; + auto i = currentTargetIndex; + do { + appendTargetWithLesserDepth(m_path[i], currentDepthAllowed); + } while (i--); + path.reverse(); + + currentDepthAllowed = currentTargetDepth; + for (auto i = currentTargetIndex + 1; i < pathSize; ++i) + appendTargetWithLesserDepth(m_path[i], currentDepthAllowed); + + return path; +} -EventPath::EventPath(const WTF::Vector& targets) +EventPath::EventPath(const Vector& targets) { m_path = targets.map([&](auto* target) { ASSERT(target); + // ASSERT(!is(target)); return EventContext { EventContext::Type::Normal, nullptr, target, *targets.begin(), 0 }; }); } +EventPath::EventPath(EventTarget& target) +{ + m_path = { EventContext { EventContext::Type::Normal, nullptr, &target, &target, 0 } }; +} + // static Node* moveOutOfAllShadowRoots(Node& startingNode) // { // Node* node = &startingNode; diff --git a/src/bun.js/bindings/webcore/EventPath.h b/src/bun.js/bindings/webcore/EventPath.h index feb86fb7bf0570..2caee6588dd9c0 100644 --- a/src/bun.js/bindings/webcore/EventPath.h +++ b/src/bun.js/bindings/webcore/EventPath.h @@ -35,13 +35,14 @@ class EventPath { public: EventPath(Node& origin, Event&); explicit EventPath(const Vector&); + explicit EventPath(EventTarget&); bool isEmpty() const { return m_path.isEmpty(); } size_t size() const { return m_path.size(); } const EventContext& contextAt(size_t i) const { return m_path[i]; } EventContext& contextAt(size_t i) { return m_path[i]; } - Vector computePathUnclosedToTarget(const EventTarget&) const; + Vector> computePathUnclosedToTarget(const EventTarget&) const; static Node* eventTargetRespectingTargetRules(Node&); diff --git a/src/bun.js/bindings/webcore/EventTarget.cpp b/src/bun.js/bindings/webcore/EventTarget.cpp index 1a859de05f15af..0ba12bf3df5d7e 100644 --- a/src/bun.js/bindings/webcore/EventTarget.cpp +++ b/src/bun.js/bindings/webcore/EventTarget.cpp @@ -31,6 +31,7 @@ #include "config.h" #include "Event.h" +#include "EventPath.h" #include "EventTarget.h" @@ -248,10 +249,12 @@ void EventTarget::dispatchEvent(Event& event) ASSERT(event.isInitialized()); ASSERT(!event.isBeingDispatched()); + EventPath eventPath(*this); event.setTarget(this); event.setCurrentTarget(this); event.setEventPhase(Event::AT_TARGET); event.resetBeforeDispatch(); + event.setEventPath(eventPath); fireEventListeners(event, EventInvokePhase::Capturing); fireEventListeners(event, EventInvokePhase::Bubbling); event.resetAfterDispatch(); diff --git a/src/bun.js/bindings/webcore/JSCustomEvent.cpp b/src/bun.js/bindings/webcore/JSCustomEvent.cpp index dbc73d293d8d75..fef4d409a8d20b 100644 --- a/src/bun.js/bindings/webcore/JSCustomEvent.cpp +++ b/src/bun.js/bindings/webcore/JSCustomEvent.cpp @@ -47,6 +47,7 @@ #include #include #include +#include "../ErrorCode.h" namespace WebCore { using namespace JSC; @@ -58,7 +59,8 @@ template<> CustomEvent::Init convertDictionary(JSGlobalObject bool isNullOrUndefined = value.isUndefinedOrNull(); auto* object = isNullOrUndefined ? nullptr : value.getObject(); if (UNLIKELY(!isNullOrUndefined && !object)) { - throwTypeError(&lexicalGlobalObject, throwScope); + Bun::throwError(&lexicalGlobalObject, throwScope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "The \"options\" argument must be of type object."_s); return {}; } CustomEvent::Init result; diff --git a/src/bun.js/bindings/webcore/JSEventTargetNode.cpp b/src/bun.js/bindings/webcore/JSEventTargetNode.cpp index 7fc9eec1d340cc..314b64d55ef1c4 100644 --- a/src/bun.js/bindings/webcore/JSEventTargetNode.cpp +++ b/src/bun.js/bindings/webcore/JSEventTargetNode.cpp @@ -8,6 +8,7 @@ #include "JSEventTarget.h" #include "JavaScriptCore/JSArray.h" #include "wtf/text/MakeString.h" +#include "../ErrorCode.h" namespace Bun { @@ -29,8 +30,10 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeEventsGetEventListeners, (JSGlobalObject auto eventType = callFrame->argument(1).toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); - if (UNLIKELY(!thisObject)) - return JSValue::encode(constructEmptyArray(globalObject, nullptr, 0)); + if (UNLIKELY(!thisObject)) { + return Bun::throwError(globalObject, throwScope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "ERR_INVALID_ARG_TYPE: first argument must be of type EventEmitter"_s); + } MarkedArgumentBuffer values; auto& listeners = thisObject->wrapped().eventListeners(WTF::makeAtomString(eventType)); diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 3059002802d328..66c40186343360 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -4018,7 +4018,8 @@ pub const ParseTask = struct { const OnBeforeParseResultWrapper = struct { original_source: ?[]const u8 = null, loader: Loader, - impl: OnBeforeParseResult, + check: if (bun.Environment.isDebug) u32 else u0 = if (bun.Environment.isDebug) 42069 else 0, // Value to ensure OnBeforeParseResult is wrapped in this struct + result: OnBeforeParseResult, }; const OnBeforeParseResult = extern struct { @@ -4027,7 +4028,7 @@ pub const ParseTask = struct { source_len: usize = 0, loader: Loader, - fetch_source_code_fn: *const fn (*const OnBeforeParseArguments, *OnBeforeParseResult) callconv(.C) i32 = &fetchSourceCode, + fetch_source_code_fn: *const fn (*OnBeforeParseArguments, *OnBeforeParseResult) callconv(.C) i32 = &fetchSourceCode, user_context: ?*anyopaque = null, free_user_context: ?*const fn (?*anyopaque) callconv(.C) void = null, @@ -4036,9 +4037,15 @@ pub const ParseTask = struct { args_: ?*OnBeforeParseArguments, log_options_: ?*BunLogOptions, ) callconv(.C) void = &BunLogOptions.logFn, + + pub fn getWrapper(result: *OnBeforeParseResult) *OnBeforeParseResultWrapper { + const wrapper: *OnBeforeParseResultWrapper = @fieldParentPtr("result", result); + bun.debugAssert(wrapper.check == 42069); + return wrapper; + } }; - pub fn fetchSourceCode(args: *const OnBeforeParseArguments, result: *OnBeforeParseResult) callconv(.C) i32 { + pub fn fetchSourceCode(args: *OnBeforeParseArguments, result: *OnBeforeParseResult) callconv(.C) i32 { debug("fetchSourceCode", .{}); const this = args.context; if (this.log.errors > 0 or this.deferred_error != null or this.should_continue_running.* != 1) { @@ -4069,17 +4076,35 @@ pub const ParseTask = struct { result.source_len = entry.contents.len; result.free_user_context = null; result.user_context = null; + const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); + wrapper.original_source = entry.contents; return 0; } + pub export fn OnBeforeParseResult__reset(this: *OnBeforeParseResult) void { + const wrapper = this.getWrapper(); + this.loader = wrapper.loader; + if (wrapper.original_source) |src| { + this.source_ptr = src.ptr; + this.source_len = src.len; + } else { + this.source_ptr = null; + this.source_len = 0; + } + } + pub export fn OnBeforeParsePlugin__isDone(this: *OnBeforeParsePlugin) i32 { if (this.should_continue_running.* != 1) { return 1; } const result = this.result orelse return 1; + // The first plugin to set the source wins. + // But, we must check that they actually modified it + // since fetching the source stores it inside `result.source_ptr` if (result.source_ptr != null) { - return 1; + const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); + return @intFromBool(result.source_ptr.? != wrapper.original_source.?.ptr); } return 0; @@ -4096,10 +4121,14 @@ pub const ParseTask = struct { args.namespace_ptr = this.file_path.namespace.ptr; args.namespace_len = this.file_path.namespace.len; } - var result = OnBeforeParseResult{ + var wrapper = OnBeforeParseResultWrapper{ .loader = this.loader.*, + .result = OnBeforeParseResult{ + .loader = this.loader.*, + }, }; - this.result = &result; + + this.result = &wrapper.result; const count = plugin.callOnBeforeParsePlugins( this, if (bun.strings.eqlComptime(this.file_path.namespace, "file")) @@ -4109,15 +4138,15 @@ pub const ParseTask = struct { &bun.String.init(this.file_path.text), &args, - &result, + &wrapper.result, this.should_continue_running, ); if (comptime Environment.enable_logs) debug("callOnBeforeParsePlugins({s}:{s}) = {d}", .{ this.file_path.namespace, this.file_path.text, count }); if (count > 0) { if (this.deferred_error) |err| { - if (result.free_user_context) |free_user_context| { - free_user_context(result.user_context); + if (wrapper.result.free_user_context) |free_user_context| { + free_user_context(wrapper.result.user_context); } return err; @@ -4125,7 +4154,7 @@ pub const ParseTask = struct { // If the plugin sets the `free_user_context` function pointer, it _must_ set the `user_context` pointer. // Otherwise this is just invalid behavior. - if (result.user_context == null and result.free_user_context != null) { + if (wrapper.result.user_context == null and wrapper.result.free_user_context != null) { var msg = Logger.Msg{ .data = .{ .location = null, .text = bun.default_allocator.dupe( u8, "Native plugin set the `free_plugin_source_code_context` field without setting the `plugin_source_code_context` field.", @@ -4137,27 +4166,27 @@ pub const ParseTask = struct { } if (this.log.errors > 0) { - if (result.free_user_context) |free_user_context| { - free_user_context(result.user_context); + if (wrapper.result.free_user_context) |free_user_context| { + free_user_context(wrapper.result.user_context); } return error.SyntaxError; } - if (result.source_ptr) |ptr| { - if (result.free_user_context != null) { + if (wrapper.result.source_ptr) |ptr| { + if (wrapper.result.free_user_context != null) { this.task.external = CacheEntry.External{ - .ctx = result.user_context, - .function = result.free_user_context, + .ctx = wrapper.result.user_context, + .function = wrapper.result.free_user_context, }; } from_plugin.* = true; - this.loader.* = result.loader; + this.loader.* = wrapper.result.loader; return CacheEntry{ - .contents = ptr[0..result.source_len], + .contents = ptr[0..wrapper.result.source_len], .external = .{ - .ctx = result.user_context, - .function = result.free_user_context, + .ctx = wrapper.result.user_context, + .function = wrapper.result.free_user_context, }, }; } diff --git a/src/css/context.zig b/src/css/context.zig index db6b3964a2a019..09c9e59373c206 100644 --- a/src/css/context.zig +++ b/src/css/context.zig @@ -25,9 +25,9 @@ pub const SupportsEntry = struct { important_declarations: ArrayList(css.Property), pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { - _ = this; // autofix - _ = allocator; // autofix - @panic(css.todo_stuff.depth); + this.condition.deinit(allocator); + css.deepDeinit(css.Property, allocator, &this.declarations); + css.deepDeinit(css.Property, allocator, &this.important_declarations); } }; diff --git a/src/css/css_modules.zig b/src/css/css_modules.zig index 14767a4a8c4a42..e476b03b4c2bd1 100644 --- a/src/css/css_modules.zig +++ b/src/css/css_modules.zig @@ -70,17 +70,71 @@ pub const CssModule = struct { // TODO: deinit } + pub fn getReference(this: *CssModule, allocator: Allocator, name: []const u8, source_index: u32) void { + const gop = this.exports_by_source_index.items[source_index].getOrPut(allocator, name) catch bun.outOfMemory(); + if (gop.found_existing) { + gop.value_ptr.is_referenced = true; + } else { + gop.value_ptr.* = CssModuleExport{ + .name = this.config.pattern.writeToString(allocator, .{}, this.hashes.items[source_index], this.sources.items[source_index], name), + .composes = .{}, + .is_referenced = true, + }; + } + } + pub fn referenceDashed( this: *CssModule, + allocator: std.mem.Allocator, name: []const u8, from: *const ?css.css_properties.css_modules.Specifier, source_index: u32, ) ?[]const u8 { - _ = this; // autofix - _ = name; // autofix - _ = from; // autofix - _ = source_index; // autofix - @panic(css.todo_stuff.depth); + const reference, const key = if (from.*) |specifier| switch (specifier) { + .global => return name[2..], + .file => |file| .{ + CssModuleReference{ .dependency = .{ .name = name[2..], .specifier = file } }, + file, + }, + .source_index => |dep_source_index| return this.config.pattern.writeToString( + allocator, + .{}, + this.hashes.items[dep_source_index], + this.sources.items[dep_source_index], + name[2..], + ), + } else { + // Local export. Mark as used. + const gop = this.exports_by_source_index.items[source_index].getOrPut(allocator, name) catch bun.outOfMemory(); + if (gop.found_existing) { + gop.value_ptr.is_referenced = true; + } else { + var res = ArrayList(u8){}; + res.appendSlice(allocator, "--") catch bun.outOfMemory(); + gop.value_ptr.* = CssModuleExport{ + .name = this.config.pattern.writeToString( + allocator, + res, + this.hashes.items[source_index], + this.sources.items[source_index], + name[2..], + ), + .composes = .{}, + .is_referenced = true, + }; + } + return null; + }; + + const the_hash = hash(allocator, "{s}_{s}_{s}", .{ this.hashes.items[source_index], name, key }, false); + + this.references.put( + allocator, + std.fmt.allocPrint(allocator, "--{s}", .{the_hash}) catch bun.outOfMemory(), + reference, + ) catch bun.outOfMemory(); + + return the_hash; } pub fn handleComposes( @@ -397,10 +451,33 @@ pub const CssModuleReference = union(enum) { // TODO: replace with bun's hash pub fn hash(allocator: Allocator, comptime fmt: []const u8, args: anytype, at_start: bool) []const u8 { - _ = fmt; // autofix - _ = args; // autofix - _ = allocator; // autofix - _ = at_start; // autofix - // @compileError(css.todo_stuff.depth); - @panic(css.todo_stuff.depth); + const count = std.fmt.count(fmt, args); + var stack_fallback = std.heap.stackFallback(128, allocator); + const fmt_alloc = if (count <= 128) stack_fallback.get() else allocator; + var hasher = bun.Wyhash11.init(0); + var fmt_str = std.fmt.allocPrint(fmt_alloc, fmt, args) catch bun.outOfMemory(); + hasher.update(fmt_str); + + const h: u32 = @truncate(hasher.final()); + var h_bytes: [4]u8 = undefined; + std.mem.writeInt(u32, &h_bytes, h, .little); + + const encode_len = bun.base64.encodeLen(h_bytes[0..]); + + var slice_to_write = if (encode_len <= 128 - @as(usize, @intFromBool(at_start))) + allocator.alloc(u8, encode_len + @as(usize, @intFromBool(at_start))) catch bun.outOfMemory() + else + fmt_str[0..]; + + const base64_encoded_hash_len = bun.base64.encode(slice_to_write, &h_bytes); + + const base64_encoded_hash = slice_to_write[0..base64_encoded_hash_len]; + + if (at_start and base64_encoded_hash.len > 0 and base64_encoded_hash[0] >= '0' and base64_encoded_hash[0] <= '9') { + std.mem.copyBackwards(u8, slice_to_write[1..][0..base64_encoded_hash_len], base64_encoded_hash); + slice_to_write[0] = '_'; + return slice_to_write[0 .. base64_encoded_hash_len + 1]; + } + + return base64_encoded_hash; } diff --git a/src/css/css_parser.zig b/src/css/css_parser.zig index ab434d3f0f09f6..40ceaa2966f33f 100644 --- a/src/css/css_parser.zig +++ b/src/css/css_parser.zig @@ -384,7 +384,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // } // return null; - @panic(todo_stuff.depth); + @compileError(todo_stuff.depth); } /// Returns a shorthand from the longhand properties defined in the given declaration block. @@ -405,7 +405,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // }; // return out; - @panic(todo_stuff.depth); + @compileError(todo_stuff.depth); } /// Returns a longhand property for this shorthand. @@ -430,7 +430,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // } // } // return null; - @panic(todo_stuff.depth); + @compileError(todo_stuff.depth); } /// Updates this shorthand from a longhand property. @@ -451,7 +451,7 @@ pub fn DefineShorthand(comptime T: type, comptime property_name: PropertyIdTag) // } // } // return false; - @panic(todo_stuff.depth); + @compileError(todo_stuff.depth); } }; } @@ -916,12 +916,8 @@ pub fn DeriveValueType(comptime T: type) type { } fn consume_until_end_of_block(block_type: BlockType, tokenizer: *Tokenizer) void { - const StackCount = 16; - var sfb = std.heap.stackFallback(@sizeOf(BlockType) * StackCount, tokenizer.allocator); - const alloc = sfb.get(); - var stack = std.ArrayList(BlockType).initCapacity(alloc, StackCount) catch unreachable; - defer stack.deinit(); - + @setCold(true); + var stack = SmallList(BlockType, 16){}; stack.appendAssumeCapacity(block_type); while (switch (tokenizer.next()) { @@ -929,13 +925,13 @@ fn consume_until_end_of_block(block_type: BlockType, tokenizer: *Tokenizer) void .err => null, }) |tok| { if (BlockType.closing(&tok)) |b| { - if (stack.getLast() == b) { + if (stack.getLastUnchecked() == b) { _ = stack.pop(); - if (stack.items.len == 0) return; + if (stack.len() == 0) return; } } - if (BlockType.opening(&tok)) |bt| stack.append(bt) catch unreachable; + if (BlockType.opening(&tok)) |bt| stack.append(tokenizer.allocator, bt); } } @@ -4200,8 +4196,6 @@ const Tokenizer = struct { .position = 0, }; - // make current point to the first token - _ = lexer.next(); lexer.position = 0; return lexer; @@ -6253,6 +6247,20 @@ pub const serializer = struct { }; } else notation: { var buf: [129]u8 = undefined; + // We must pass finite numbers to dtoa_short + if (std.math.isPositiveInf(value)) { + const output = "1e999"; + try writer.writeAll(output); + return; + } else if (std.math.isNegativeInf(value)) { + const output = "-1e999"; + try writer.writeAll(output); + return; + } + // We shouldn't receive NaN here. + // NaN is not a valid CSS token and any inlined calculations from `calc()` we ensure + // are not NaN. + bun.debugAssert(!std.math.isNan(value)); const str, const notation = dtoa_short(&buf, value, 6); try writer.writeAll(str); break :notation notation; @@ -6691,6 +6699,7 @@ const Notation = struct { pub fn dtoa_short(buf: *[129]u8, value: f32, comptime precision: u8) struct { []u8, Notation } { buf[0] = '0'; + bun.debugAssert(std.math.isFinite(value)); const buf_len = bun.fmt.FormatDouble.dtoa(@ptrCast(buf[1..].ptr), @floatCast(value)).len; return restrict_prec(buf[0 .. buf_len + 1], precision); } diff --git a/src/css/properties/animation.zig b/src/css/properties/animation.zig index b6136db261854f..d6d8fb198f4820 100644 --- a/src/css/properties/animation.zig +++ b/src/css/properties/animation.zig @@ -47,9 +47,44 @@ pub const AnimationName = union(enum) { } pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { - _ = this; // autofix - _ = dest; // autofix - @panic(css.todo_stuff.depth); + const css_module_animation_enabled = if (dest.css_module) |css_module| + css_module.config.animation + else + false; + + switch (this.*) { + .none => return dest.writeStr("none"), + .ident => |s| { + if (css_module_animation_enabled) { + if (dest.css_module) |*css_module| { + css_module.getReference(dest.allocator, s.v, dest.loc.source_index); + } + } + return s.toCssWithOptions(W, dest, css_module_animation_enabled); + }, + .string => |s| { + if (css_module_animation_enabled) { + if (dest.css_module) |*css_module| { + css_module.getReference(dest.allocator, s, dest.loc.source_index); + } + } + + // CSS-wide keywords and `none` cannot remove quotes + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "none") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "initial") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "inherit") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "unset") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "default") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "revert") or + bun.strings.eqlCaseInsensitiveASCIIICheckLength(s, "revert-layer")) + { + css.serializer.serializeString(s, dest) catch return dest.addFmtError(); + return; + } + + return dest.writeIdent(s, css_module_animation_enabled); + }, + } } }; diff --git a/src/css/properties/css_modules.zig b/src/css/properties/css_modules.zig index fa087a3866df10..859ca91f577a25 100644 --- a/src/css/properties/css_modules.zig +++ b/src/css/properties/css_modules.zig @@ -40,8 +40,20 @@ pub const Composes = struct { loc: Location, pub fn parse(input: *css.Parser) css.Result(Composes) { - _ = input; // autofix - @panic(css.todo_stuff.depth); + const loc = input.currentSourceLocation(); + var names: CustomIdentList = .{}; + while (input.tryParse(parseOneIdent, .{}).asValue()) |name| { + names.append(input.allocator(), name); + } + + if (names.len() == 0) return .{ .err = input.newCustomError(css.ParserError{ .invalid_declaration = {} }) }; + + const from = if (input.tryParse(css.Parser.expectIdentMatching, .{"from"}).isOk()) switch (Specifier.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } else null; + + return .{ .result = Composes{ .names = names, .from = from, .loc = Location.fromSourceLocation(loc) } }; } pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { @@ -61,6 +73,17 @@ pub const Composes = struct { } } + fn parseOneIdent(input: *css.Parser) css.Result(CustomIdent) { + const name: CustomIdent = switch (CustomIdent.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + if (bun.strings.eqlCaseInsensitiveASCII(name.v, "from", true)) return .{ .err = input.newErrorForNextToken() }; + + return .{ .result = name }; + } + pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { return css.implementDeepClone(@This(), this, allocator); } diff --git a/src/css/properties/font.zig b/src/css/properties/font.zig index fe1980e19aa2d0..b5e8f12effca4e 100644 --- a/src/css/properties/font.zig +++ b/src/css/properties/font.zig @@ -160,17 +160,25 @@ pub const FontStretch = union(enum) { percentage: Percentage, // TODO: implement this - // pub usingnamespace css.DeriveParse(@This()); + pub usingnamespace css.DeriveParse(@This()); + + pub fn toCss(this: *const FontStretch, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { + if (dest.minify) { + const percentage: Percentage = this.intoPercentage(); + return percentage.toCss(W, dest); + } - pub fn parse(input: *css.Parser) css.Result(FontStretch) { - _ = input; // autofix - @panic(css.todo_stuff.depth); + return switch (this.*) { + .percentage => |*val| val.toCss(W, dest), + .keyword => |*kw| kw.toCss(W, dest), + }; } - pub fn toCss(this: *const FontStretch, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { - _ = this; // autofix - _ = dest; // autofix - @panic(css.todo_stuff.depth); + pub fn intoPercentage(this: *const FontStretch) Percentage { + return switch (this.*) { + .percentage => |*val| val.*, + .keyword => |*kw| kw.intoPercentage(), + }; } pub fn eql(lhs: *const FontStretch, rhs: *const FontStretch) bool { @@ -215,6 +223,21 @@ pub const FontStretchKeyword = enum { pub inline fn default() FontStretchKeyword { return .normal; } + + pub fn intoPercentage(this: *const FontStretchKeyword) Percentage { + const val: f32 = switch (this.*) { + .@"ultra-condensed" => 0.5, + .@"extra-condensed" => 0.625, + .condensed => 0.75, + .@"semi-condensed" => 0.875, + .normal => 1.0, + .@"semi-expanded" => 1.125, + .expanded => 1.25, + .@"extra-expanded" => 1.5, + .@"ultra-expanded" => 2.0, + }; + return .{ .v = val }; + } }; /// A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property. diff --git a/src/css/properties/transform.zig b/src/css/properties/transform.zig index 576779ad30c5c7..bcc9ebce9984cb 100644 --- a/src/css/properties/transform.zig +++ b/src/css/properties/transform.zig @@ -38,14 +38,62 @@ pub const TransformList = struct { v: ArrayList(Transform), pub fn parse(input: *css.Parser) Result(@This()) { - _ = input; // autofix - @panic(css.todo_stuff.depth); + if (input.tryParse(css.Parser.expectIdentMatching, .{"none"}).isOk()) { + return .{ .result = .{ .v = .{} } }; + } + + input.skipWhitespace(); + var results = ArrayList(Transform){}; + switch (Transform.parse(input)) { + .result => |first| results.append(input.allocator(), first) catch bun.outOfMemory(), + .err => |e| return .{ .err = e }, + } + + while (true) { + input.skipWhitespace(); + if (input.tryParse(Transform.parse, .{}).asValue()) |item| { + results.append(input.allocator(), item) catch bun.outOfMemory(); + } else { + return .{ .result = .{ .v = results } }; + } + } } pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { - _ = this; // autofix - _ = dest; // autofix - @panic(css.todo_stuff.depth); + if (this.v.items.len == 0) { + return dest.writeStr("none"); + } + + // TODO: Re-enable with a better solution + // See: https://github.com/parcel-bundler/lightningcss/issues/288 + if (dest.minify) { + var base = ArrayList(u8){}; + const base_writer = base.writer(dest.allocator); + const WW = @TypeOf(base_writer); + + var scratchbuf = std.ArrayList(u8).init(dest.allocator); + defer scratchbuf.deinit(); + var p = Printer(WW).new( + dest.allocator, + scratchbuf, + base_writer, + css.PrinterOptions{ + .minify = true, + }, + dest.import_records, + ); + defer p.deinit(); + + try this.toCssBase(WW, &p); + + return dest.writeStr(base.items); + } + } + + fn toCssBase(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { + for (this.v.items) |*item| { + try item.toCss(W, dest); + } } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { @@ -145,14 +193,583 @@ pub const Transform = union(enum) { matrix_3d: Matrix3d(f32), pub fn parse(input: *css.Parser) Result(Transform) { - _ = input; // autofix - @panic(css.todo_stuff.depth); + const function = switch (input.expectFunction()) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + + const Closure = struct { function: []const u8 }; + return input.parseNestedBlock( + Transform, + Closure{ .function = function }, + struct { + fn parse(closure: Closure, i: *css.Parser) css.Result(Transform) { + const location = i.currentSourceLocation(); + if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "matrix")) { + const a = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const b = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const c = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const d = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const e = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |ee| return .{ .err = ee }; + const f = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |ee| return .{ .err = ee }, + }; + return .{ .result = .{ .matrix = .{ .a = a, .b = b, .c = c, .d = d, .e = e, .f = f } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "matrix3d")) { + const m11 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m12 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m13 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m14 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m21 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m22 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m23 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m24 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m31 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m32 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m33 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m34 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m41 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m42 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m43 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const m44 = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .matrix_3d = .{ + .m11 = m11, + .m12 = m12, + .m13 = m13, + .m14 = m14, + .m21 = m21, + .m22 = m22, + .m23 = m23, + .m24 = m24, + .m31 = m31, + .m32 = m32, + .m33 = m33, + .m34 = m34, + .m41 = m41, + .m42 = m42, + .m43 = m43, + .m44 = m44, + } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translate")) { + const x = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.tryParse(struct { + fn parse(p: *css.Parser) css.Result(void) { + return p.expectComma(); + } + }.parse, .{}).isOk()) { + const y = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate = .{ .x = x, .y = y } } }; + } else { + return .{ .result = .{ .translate = .{ .x = x, .y = LengthPercentage.zero() } } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translatex")) { + const x = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate_x = x } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translatey")) { + const y = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate_y = y } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translatez")) { + const z = switch (Length.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate_z = z } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "translate3d")) { + const x = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const y = switch (LengthPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const z = switch (Length.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .translate_3d = .{ .x = x, .y = y, .z = z } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scale")) { + const x = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.tryParse(struct { + fn parse(p: *css.Parser) css.Result(void) { + return p.expectComma(); + } + }.parse, .{}).isOk()) { + const y = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale = .{ .x = x, .y = y } } }; + } else { + return .{ .result = .{ .scale = .{ .x = x, .y = x.deepClone(i.allocator()) } } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scalex")) { + const x = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale_x = x } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scaley")) { + const y = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale_y = y } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scalez")) { + const z = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale_z = z } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "scale3d")) { + const x = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const y = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const z = switch (NumberOrPercentage.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .scale_3d = .{ .x = x, .y = y, .z = z } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotate")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotatex")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate_x = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotatey")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate_y = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotatez")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate_z = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "rotate3d")) { + const x = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const y = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const z = switch (css.CSSNumberFns.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.expectComma().asErr()) |e| return .{ .err = e }; + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .rotate_3d = .{ .x = x, .y = y, .z = z, .angle = angle } } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "skew")) { + const x = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + if (i.tryParse(struct { + fn parse(p: *css.Parser) css.Result(void) { + return p.expectComma(); + } + }.parse, .{}).isOk()) { + const y = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .skew = .{ .x = x, .y = y } } }; + } else { + return .{ .result = .{ .skew = .{ .x = x, .y = Angle{ .deg = 0.0 } } } }; + } + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "skewx")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .skew_x = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "skewy")) { + const angle = switch (Angle.parseWithUnitlessZero(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .skew_y = angle } }; + } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.function, "perspective")) { + const len = switch (Length.parse(i)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + }; + return .{ .result = .{ .perspective = len } }; + } else { + return .{ .err = location.newUnexpectedTokenError(.{ .ident = closure.function }) }; + } + } + }.parse, + ); } pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { - _ = this; // autofix - _ = dest; // autofix - @panic(css.todo_stuff.depth); + switch (this.*) { + .translate => |t| { + if (dest.minify and t.x.isZero() and !t.y.isZero()) { + try dest.writeStr("translateY("); + try t.y.toCss(W, dest); + } else { + try dest.writeStr("translate("); + try t.x.toCss(W, dest); + if (!t.y.isZero()) { + try dest.delim(',', false); + try t.y.toCss(W, dest); + } + } + try dest.writeChar(')'); + }, + .translate_x => |x| { + try dest.writeStr(if (dest.minify) "translate(" else "translateX("); + try x.toCss(W, dest); + try dest.writeChar(')'); + }, + .translate_y => |y| { + try dest.writeStr("translateY("); + try y.toCss(W, dest); + try dest.writeChar(')'); + }, + .translate_z => |z| { + try dest.writeStr("translateZ("); + try z.toCss(W, dest); + try dest.writeChar(')'); + }, + .translate_3d => |t| { + if (dest.minify and !t.x.isZero() and t.y.isZero() and t.z.isZero()) { + try dest.writeStr("translate("); + try t.x.toCss(W, dest); + } else if (dest.minify and t.x.isZero() and !t.y.isZero() and t.z.isZero()) { + try dest.writeStr("translateY("); + try t.y.toCss(W, dest); + } else if (dest.minify and t.x.isZero() and t.y.isZero() and !t.z.isZero()) { + try dest.writeStr("translateZ("); + try t.z.toCss(W, dest); + } else if (dest.minify and t.z.isZero()) { + try dest.writeStr("translate("); + try t.x.toCss(W, dest); + try dest.delim(',', false); + try t.y.toCss(W, dest); + } else { + try dest.writeStr("translate3d("); + try t.x.toCss(W, dest); + try dest.delim(',', false); + try t.y.toCss(W, dest); + try dest.delim(',', false); + try t.z.toCss(W, dest); + } + try dest.writeChar(')'); + }, + .scale => |s| { + const x: f32 = s.x.intoF32(); + const y: f32 = s.y.intoF32(); + if (dest.minify and x == 1.0 and y != 1.0) { + try dest.writeStr("scaleY("); + try css.CSSNumberFns.toCss(&y, W, dest); + } else if (dest.minify and x != 1.0 and y == 1.0) { + try dest.writeStr("scaleX("); + try css.CSSNumberFns.toCss(&x, W, dest); + } else { + try dest.writeStr("scale("); + try css.CSSNumberFns.toCss(&x, W, dest); + if (y != x) { + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&y, W, dest); + } + } + try dest.writeChar(')'); + }, + .scale_x => |x| { + try dest.writeStr("scaleX("); + try css.CSSNumberFns.toCss(&x.intoF32(), W, dest); + try dest.writeChar(')'); + }, + .scale_y => |y| { + try dest.writeStr("scaleY("); + try css.CSSNumberFns.toCss(&y.intoF32(), W, dest); + try dest.writeChar(')'); + }, + .scale_z => |z| { + try dest.writeStr("scaleZ("); + try css.CSSNumberFns.toCss(&z.intoF32(), W, dest); + try dest.writeChar(')'); + }, + .scale_3d => |s| { + const x: f32 = s.x.intoF32(); + const y: f32 = s.y.intoF32(); + const z: f32 = s.z.intoF32(); + if (dest.minify and z == 1.0 and x == y) { + try dest.writeStr("scale("); + try css.CSSNumberFns.toCss(&x, W, dest); + } else if (dest.minify and x != 1.0 and y == 1.0 and z == 1.0) { + try dest.writeStr("scaleX("); + try css.CSSNumberFns.toCss(&x, W, dest); + } else if (dest.minify and x == 1.0 and y != 1.0 and z == 1.0) { + try dest.writeStr("scaleY("); + try css.CSSNumberFns.toCss(&y, W, dest); + } else if (dest.minify and x == 1.0 and y == 1.0 and z != 1.0) { + try dest.writeStr("scaleZ("); + try css.CSSNumberFns.toCss(&z, W, dest); + } else if (dest.minify and z == 1.0) { + try dest.writeStr("scale("); + try css.CSSNumberFns.toCss(&x, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&y, W, dest); + } else { + try dest.writeStr("scale3d("); + try css.CSSNumberFns.toCss(&x, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&y, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&z, W, dest); + } + try dest.writeChar(')'); + }, + .rotate => |angle| { + try dest.writeStr("rotate("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .rotate_x => |angle| { + try dest.writeStr("rotateX("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .rotate_y => |angle| { + try dest.writeStr("rotateY("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .rotate_z => |angle| { + try dest.writeStr(if (dest.minify) "rotate(" else "rotateZ("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .rotate_3d => |r| { + if (dest.minify and r.x == 1.0 and r.y == 0.0 and r.z == 0.0) { + try dest.writeStr("rotateX("); + try r.angle.toCssWithUnitlessZero(W, dest); + } else if (dest.minify and r.x == 0.0 and r.y == 1.0 and r.z == 0.0) { + try dest.writeStr("rotateY("); + try r.angle.toCssWithUnitlessZero(W, dest); + } else if (dest.minify and r.x == 0.0 and r.y == 0.0 and r.z == 1.0) { + try dest.writeStr("rotate("); + try r.angle.toCssWithUnitlessZero(W, dest); + } else { + try dest.writeStr("rotate3d("); + try css.CSSNumberFns.toCss(&r.x, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&r.y, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&r.z, W, dest); + try dest.delim(',', false); + try r.angle.toCssWithUnitlessZero(W, dest); + } + try dest.writeChar(')'); + }, + .skew => |s| { + if (dest.minify and s.x.isZero() and !s.y.isZero()) { + try dest.writeStr("skewY("); + try s.y.toCssWithUnitlessZero(W, dest); + } else { + try dest.writeStr("skew("); + try s.x.toCss(W, dest); + if (!s.y.isZero()) { + try dest.delim(',', false); + try s.y.toCssWithUnitlessZero(W, dest); + } + } + try dest.writeChar(')'); + }, + .skew_x => |angle| { + try dest.writeStr(if (dest.minify) "skew(" else "skewX("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .skew_y => |angle| { + try dest.writeStr("skewY("); + try angle.toCssWithUnitlessZero(W, dest); + try dest.writeChar(')'); + }, + .perspective => |len| { + try dest.writeStr("perspective("); + try len.toCss(W, dest); + try dest.writeChar(')'); + }, + .matrix => |m| { + try dest.writeStr("matrix("); + try css.CSSNumberFns.toCss(&m.a, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.b, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.c, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.d, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.e, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.f, W, dest); + try dest.writeChar(')'); + }, + .matrix_3d => |m| { + try dest.writeStr("matrix3d("); + try css.CSSNumberFns.toCss(&m.m11, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m12, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m13, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m14, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m21, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m22, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m23, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m24, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m31, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m32, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m33, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m34, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m41, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m42, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m43, W, dest); + try dest.delim(',', false); + try css.CSSNumberFns.toCss(&m.m44, W, dest); + try dest.writeChar(')'); + }, + } } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() { diff --git a/src/css/rules/keyframes.zig b/src/css/rules/keyframes.zig index 640683d41a88ca..d79eca6a7ab54b 100644 --- a/src/css/rules/keyframes.zig +++ b/src/css/rules/keyframes.zig @@ -306,7 +306,7 @@ pub const KeyframesRule = struct { pub fn getFallbacks(this: *This, comptime T: type, targets: *const css.targets.Targets) []css.CssRule(T) { _ = this; // autofix _ = targets; // autofix - @panic(css.todo_stuff.depth); + @compileError(css.todo_stuff.depth); } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) This { diff --git a/src/css/rules/property.zig b/src/css/rules/property.zig index b3044d183657c8..8ad0892f10c147 100644 --- a/src/css/rules/property.zig +++ b/src/css/rules/property.zig @@ -123,7 +123,7 @@ pub const PropertyRule = struct { dest.dedent(); try dest.newline(); - try dest.writeChar(';'); + try dest.writeChar('}'); } pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) This { diff --git a/src/css/rules/supports.zig b/src/css/rules/supports.zig index a07a78494ebfdb..126720002bd301 100644 --- a/src/css/rules/supports.zig +++ b/src/css/rules/supports.zig @@ -59,6 +59,21 @@ pub const SupportsCondition = union(enum) { /// An unknown condition. unknown: []const u8, + pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { + switch (this.*) { + .not => |not| { + not.deinit(allocator); + allocator.destroy(not); + }, + inline .@"and", .@"or" => |*list| { + css.deepDeinit(SupportsCondition, allocator, list); + }, + .declaration => {}, + .selector => {}, + .unknown => {}, + } + } + pub fn eql(this: *const SupportsCondition, other: *const SupportsCondition) bool { return css.implementEql(SupportsCondition, this, other); } diff --git a/src/css/selectors/parser.zig b/src/css/selectors/parser.zig index a89dd6345a68bf..c545c826b9a9b4 100644 --- a/src/css/selectors/parser.zig +++ b/src/css/selectors/parser.zig @@ -1050,9 +1050,7 @@ pub const SelectorParser = struct { pub fn parseFunctionalPseudoElement(this: *SelectorParser, name: []const u8, input: *css.Parser) Result(Impl.SelectorImpl.PseudoElement) { _ = this; // autofix - _ = name; // autofix - _ = input; // autofix - @panic(css.todo_stuff.depth); + return .{ .err = input.newCustomError(SelectorParseErrorKind.intoDefaultParserError(.{ .unsupported_pseudo_class_or_element = name })) }; } fn parseIsAndWhere(this: *const SelectorParser) bool { diff --git a/src/css/small_list.zig b/src/css/small_list.zig index fbc86efdd03e8d..bb48ca8593953a 100644 --- a/src/css/small_list.zig +++ b/src/css/small_list.zig @@ -87,6 +87,11 @@ pub fn SmallList(comptime T: type, comptime N: comptime_int) type { return ret; } + pub inline fn getLastUnchecked(this: *const @This()) T { + if (this.spilled()) return this.data.heap.ptr[this.data.heap.len - 1]; + return this.data.inlined[this.capacity - 1]; + } + pub inline fn at(this: *const @This(), idx: u32) *const T { return &this.as_const_ptr()[idx]; } @@ -173,6 +178,7 @@ pub fn SmallList(comptime T: type, comptime N: comptime_int) type { pub inline fn helper(comptime prefix: []const u8, pfs: *css.VendorPrefix, pfi: *const SmallList(T, 1), r: *bun.BabyList(This), alloc: Allocator) void { if (pfs.contains(css.VendorPrefix.fromName(prefix))) { var images = SmallList(T, 1).initCapacity(alloc, pfi.len()); + images.setLen(pfi.len()); for (images.slice_mut(), pfi.slice()) |*out, *in| { const image = in.getImage().getPrefixed(alloc, css.VendorPrefix.fromName(prefix)); out.* = in.withImage(alloc, image); @@ -431,6 +437,14 @@ pub fn SmallList(comptime T: type, comptime N: comptime_int) type { len_ptr.* += 1; } + pub fn pop(this: *@This()) ?T { + const ptr, const len_ptr, _ = this.tripleMut(); + if (len_ptr.* == 0) return null; + const last_index = len_ptr.* - 1; + len_ptr.* = last_index; + return ptr[last_index]; + } + pub fn append(this: *@This(), allocator: Allocator, item: T) void { var ptr, var len_ptr, const capp = this.tripleMut(); if (len_ptr.* == capp) { diff --git a/src/css/values/easing.zig b/src/css/values/easing.zig index 4d7c7cd00c9b1f..b5e5ea51c2a3e7 100644 --- a/src/css/values/easing.zig +++ b/src/css/values/easing.zig @@ -228,7 +228,7 @@ pub const StepPosition = enum { pub fn toCss(this: *const StepPosition, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { _ = this; // autofix _ = dest; // autofix - @panic(css.todo_stuff.depth); + @compileError(css.todo_stuff.depth); } pub fn parse(input: *css.Parser) Result(StepPosition) { diff --git a/src/css/values/gradient.zig b/src/css/values/gradient.zig index 292db6ec8814cd..0910a65a780c19 100644 --- a/src/css/values/gradient.zig +++ b/src/css/values/gradient.zig @@ -49,105 +49,146 @@ pub const Gradient = union(enum) { closure: Closure, input_: *css.Parser, ) Result(Gradient) { - // css.todo_stuff.match_ignore_ascii_case - if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "linear-gradient")) { - return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-linear-gradient")) { - return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "radial-gradient")) { - return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-radial-gradient")) { - return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "conic-gradient")) { - return .{ .result = .{ .conic = switch (ConicGradient.parse(input_)) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "repeating-conic-gradient")) { - return .{ .result = .{ .repeating_conic = switch (ConicGradient.parse(input_)) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-linear-gradient")) { - return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-repeating-linear-gradient")) { - return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-radial-gradient")) { - return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-repeating-radial-gradient")) { - return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-linear-gradient")) { - return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .moz = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-repeating-linear-gradient")) { - return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .moz = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-radial-gradient")) { - return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .moz = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-moz-repeating-radial-gradient")) { - return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .moz = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-linear-gradient")) { - return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-repeating-linear-gradient")) { - return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-radial-gradient")) { - return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-o-repeating-radial-gradient")) { - return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(closure.func, "-webkit-gradient")) { - return .{ .result = .{ .@"webkit-gradient" = switch (WebKitGradient.parse(input_)) { - .result => |vv| vv, - .err => |e| return .{ .err = e }, - } } }; - } else { + const Map = comptime bun.ComptimeEnumMap(enum { + @"linear-gradient", + @"repeating-linear-gradient", + @"radial-gradient", + @"repeating-radial-gradient", + @"conic-gradient", + @"repeating-conic-gradient", + @"-webkit-linear-gradient", + @"-webkit-repeating-linear-gradient", + @"-webkit-radial-gradient", + @"-webkit-repeating-radial-gradient", + @"-moz-linear-gradient", + @"-moz-repeating-linear-gradient", + @"-moz-radial-gradient", + @"-moz-repeating-radial-gradient", + @"-o-linear-gradient", + @"-o-repeating-linear-gradient", + @"-o-radial-gradient", + @"-o-repeating-radial-gradient", + @"-webkit-gradient", + }); + if (Map.getAnyCase(closure.func)) |matched| + switch (matched) { + .@"linear-gradient" => { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"repeating-linear-gradient" => { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"radial-gradient" => { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"repeating-radial-gradient" => { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .none = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"conic-gradient" => { + return .{ .result = .{ .conic = switch (ConicGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"repeating-conic-gradient" => { + return .{ .result = .{ .repeating_conic = switch (ConicGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-linear-gradient" => { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-repeating-linear-gradient" => { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-radial-gradient" => { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-repeating-radial-gradient" => { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .webkit = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-moz-linear-gradient" => { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .moz = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-moz-repeating-linear-gradient" => { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .moz = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-moz-radial-gradient" => { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .moz = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-moz-repeating-radial-gradient" => { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .moz = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-o-linear-gradient" => { + return .{ .result = .{ .linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-o-repeating-linear-gradient" => { + return .{ .result = .{ .repeating_linear = switch (LinearGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-o-radial-gradient" => { + return .{ .result = .{ .radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-o-repeating-radial-gradient" => { + return .{ .result = .{ .repeating_radial = switch (RadialGradient.parse(input_, css.VendorPrefix{ .o = true })) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + .@"-webkit-gradient" => { + return .{ .result = .{ .@"webkit-gradient" = switch (WebKitGradient.parse(input_)) { + .result => |vv| vv, + .err => |e| return .{ .err = e }, + } } }; + }, + } + else return .{ .err = closure.location.newUnexpectedTokenError(.{ .ident = closure.func }) }; - } } }.parse); } @@ -157,7 +198,7 @@ pub const Gradient = union(enum) { .linear => |g| .{ "linear-gradient(", g.vendor_prefix }, .repeating_linear => |g| .{ "repeating-linear-gradient(", g.vendor_prefix }, .radial => |g| .{ "radial-gradient(", g.vendor_prefix }, - .repeating_radial => |g| .{ "repeating-linear-gradient(", g.vendor_prefix }, + .repeating_radial => |g| .{ "repeating-radial-gradient(", g.vendor_prefix }, .conic => .{ "conic-gradient(", null }, .repeating_conic => .{ "repeating-conic-gradient(", null }, .@"webkit-gradient" => .{ "-webkit-gradient(", null }, @@ -171,7 +212,7 @@ pub const Gradient = union(enum) { switch (this.*) { .linear, .repeating_linear => |*linear| { - try linear.toCss(W, dest, linear.vendor_prefix.eq(css.VendorPrefix{ .none = true })); + try linear.toCss(W, dest, linear.vendor_prefix.neq(css.VendorPrefix{ .none = true })); }, .radial, .repeating_radial => |*radial| { try radial.toCss(W, dest); @@ -475,7 +516,7 @@ pub const RadialGradient = struct { } pub fn toCss(this: *const RadialGradient, comptime W: type, dest: *Printer(W)) PrintErr!void { - if (std.meta.eql(this.shape, EndingShape.default())) { + if (!std.meta.eql(this.shape, EndingShape.default())) { try this.shape.toCss(W, dest); if (this.position.isCenter()) { try dest.delim(',', false); @@ -544,16 +585,16 @@ pub const ConicGradient = struct { // https://w3c.github.io/csswg-drafts/css-images-4/#valdef-conic-gradient-angle return Angle.parseWithUnitlessZero(i); } - }.parse, .{}).unwrapOr(Angle{ .deg = 0.0 }); + }.parse, .{}); const position = input.tryParse(struct { inline fn parse(i: *css.Parser) Result(Position) { if (i.expectIdentMatching("at").asErr()) |e| return .{ .err = e }; return Position.parse(i); } - }.parse, .{}).unwrapOr(Position.center()); + }.parse, .{}); - if (!angle.eql(&Angle{ .deg = 0.0 }) or !std.meta.eql(position, Position.center())) { + if (angle.isOk() or position.isOk()) { if (input.expectComma().asErr()) |e| return .{ .err = e }; } @@ -562,8 +603,8 @@ pub const ConicGradient = struct { .err => |e| return .{ .err = e }, }; return .{ .result = ConicGradient{ - .angle = angle, - .position = position, + .angle = angle.unwrapOr(Angle{ .deg = 0.0 }), + .position = position.unwrapOr(Position.center()), .items = items, } }; } @@ -1346,7 +1387,7 @@ pub fn ColorStop(comptime D: type) type { pub fn toCss(this: *const This, comptime W: type, dest: *Printer(W)) PrintErr!void { try this.color.toCss(W, dest); if (this.position) |*position| { - try dest.delim(',', false); + try dest.writeChar(' '); try css.generic.toCss(D, position, W, dest); } return; diff --git a/src/css/values/ident.zig b/src/css/values/ident.zig index ee861540c98eda..9dab1de5be444e 100644 --- a/src/css/values/ident.zig +++ b/src/css/values/ident.zig @@ -49,7 +49,7 @@ pub const DashedIdentReference = struct { pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void { if (dest.css_module) |*css_module| { if (css_module.config.dashed_idents) { - if (css_module.referenceDashed(this.ident.v, &this.from, dest.loc.source_index)) |name| { + if (css_module.referenceDashed(dest.allocator, this.ident.v, &this.from, dest.loc.source_index)) |name| { try dest.writeStr("--"); css.serializer.serializeName(name, dest) catch return dest.addFmtError(); return; diff --git a/src/css/values/percentage.zig b/src/css/values/percentage.zig index bdafae93e6420d..081206f7d0bf01 100644 --- a/src/css/values/percentage.zig +++ b/src/css/values/percentage.zig @@ -479,6 +479,10 @@ pub const NumberOrPercentage = union(enum) { // @panic(css.todo_stuff.depth); // } + pub fn deepClone(this: *const NumberOrPercentage, allocator: std.mem.Allocator) NumberOrPercentage { + return css.implementDeepClone(@This(), this, allocator); + } + pub fn eql(this: *const NumberOrPercentage, other: *const NumberOrPercentage) bool { return switch (this.*) { .number => |*a| switch (other.*) { diff --git a/src/css/values/syntax.zig b/src/css/values/syntax.zig index 5f8a74336709bd..ee46b209dae436 100644 --- a/src/css/values/syntax.zig +++ b/src/css/values/syntax.zig @@ -88,7 +88,7 @@ pub const SyntaxString = union(enum) { // PERF(alloc): count first? while (true) { - const component = switch (SyntaxComponent.parseString(trimmed_input)) { + const component = switch (SyntaxComponent.parseString(&trimmed_input)) { .result => |v| v, .err => |e| return .{ .err = e }, }; @@ -260,8 +260,7 @@ pub const SyntaxComponent = struct { kind: SyntaxComponentKind, multiplier: Multiplier, - pub fn parseString(input_: []const u8) css.Maybe(SyntaxComponent, void) { - var input = input_; + pub fn parseString(input: *[]const u8) css.Maybe(SyntaxComponent, void) { const kind = switch (SyntaxComponentKind.parseString(input)) { .result => |vv| vv, .err => |e| return .{ .err = e }, @@ -276,11 +275,11 @@ pub const SyntaxComponent = struct { } var multiplier: Multiplier = .none; - if (bun.strings.startsWithChar(input, '+')) { - input = input[1..]; + if (bun.strings.startsWithChar(input.*, '+')) { + input.* = input.*[1..]; multiplier = .space; - } else if (bun.strings.startsWithChar(input, '#')) { - input = input[1..]; + } else if (bun.strings.startsWithChar(input.*, '#')) { + input.* = input.*[1..]; multiplier = .comma; } @@ -334,13 +333,13 @@ pub const SyntaxComponentKind = union(enum) { /// A literal component. literal: []const u8, - pub fn parseString(input_: []const u8) css.Maybe(SyntaxComponentKind, void) { + pub fn parseString(input: *[]const u8) css.Maybe(SyntaxComponentKind, void) { // https://drafts.css-houdini.org/css-properties-values-api/#consume-syntax-component - var input = std.mem.trimLeft(u8, input_, SPACE_CHARACTERS); - if (bun.strings.startsWithChar(input, '<')) { + input.* = std.mem.trimLeft(u8, input.*, SPACE_CHARACTERS); + if (bun.strings.startsWithChar(input.*, '<')) { // https://drafts.css-houdini.org/css-properties-values-api/#consume-data-type-name - const end_idx = std.mem.indexOfScalar(u8, input, '>') orelse return .{ .err = {} }; - const name = input[1..end_idx]; + const end_idx = std.mem.indexOfScalar(u8, input.*, '>') orelse return .{ .err = {} }; + const name = input.*[1..end_idx]; // todo_stuff.match_ignore_ascii_case const component: SyntaxComponentKind = if (bun.strings.eqlCaseInsensitiveASCIIICheckLength(name, "length")) .length @@ -373,17 +372,17 @@ pub const SyntaxComponentKind = union(enum) { else return .{ .err = {} }; - input = input[end_idx + 1 ..]; + input.* = input.*[end_idx + 1 ..]; return .{ .result = component }; - } else if (input.len > 0 and isIdentStart(input[0])) { + } else if (input.len > 0 and isIdentStart(input.*[0])) { // A literal. var end_idx: usize = 0; while (end_idx < input.len and - isNameCodePoint(input[end_idx])) : (end_idx += - bun.strings.utf8ByteSequenceLengthUnsafe(input[end_idx])) + isNameCodePoint(input.*[end_idx])) : (end_idx += + bun.strings.utf8ByteSequenceLengthUnsafe(input.*[end_idx])) {} - const literal = input[0..end_idx]; - input = input[end_idx..]; + const literal = input.*[0..end_idx]; + input.* = input.*[end_idx..]; return .{ .result = SyntaxComponentKind{ .literal = literal } }; } else { return .{ .err = {} }; diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index 5918f68078833f..806b3ba117731f 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -1131,7 +1131,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { // this function encodes to UTF-16 if > 127 // so we don't need to worry about latin1 non-ascii code points // we avoid trim since we wanna keep the utf8 validation intact - const utf16_bytes_ = strings.toUTF16AllocNoTrim(bun.default_allocator, data_, true, false) catch { + const utf16_bytes_ = strings.toUTF16Alloc(bun.default_allocator, data_, true, false) catch { this.terminate(ErrorCode.invalid_utf8); return; }; diff --git a/src/js/builtins/BundlerPlugin.ts b/src/js/builtins/BundlerPlugin.ts index ed2c5e653b5b8d..a4ded7976c1367 100644 --- a/src/js/builtins/BundlerPlugin.ts +++ b/src/js/builtins/BundlerPlugin.ts @@ -1,11 +1,4 @@ -import type { - BuildConfig, - BunPlugin, - OnLoadCallback, - OnResolveCallback, - PluginBuilder, - PluginConstraints, -} from "bun"; +import type { BuildConfig, BunPlugin, OnLoadCallback, OnResolveCallback, PluginBuilder, PluginConstraints } from "bun"; type AnyFunction = (...args: any[]) => any; interface BundlerPlugin { @@ -73,8 +66,10 @@ export function runSetupFunction( if (map === onBeforeParsePlugins) { isOnBeforeParse = true; // TODO: how to check if it a napi module here? - if (!callback) { - throw new TypeError("onBeforeParse `napiModule` must be a Napi module"); + if (!callback || !$isObject(callback) || !callback.$napiDlopenHandle) { + throw new TypeError( + "onBeforeParse `napiModule` must be a Napi module which exports the `BUN_PLUGIN_NAME` symbol.", + ); } if (typeof symbol !== "string") { @@ -134,7 +129,7 @@ export function runSetupFunction( const self = this; function onStart(callback) { - if(isBake) { + if (isBake) { throw new TypeError("onStart() is not supported in Bake yet"); } if (!$isCallable(callback)) { @@ -370,7 +365,14 @@ export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespa } } -export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespace, defaultLoaderId, isServerSide: boolean) { +export function runOnLoadPlugins( + this: BundlerPlugin, + internalID, + path, + namespace, + defaultLoaderId, + isServerSide: boolean, +) { const LOADERS_MAP = $LoaderLabelToId; const loaderName = $LoaderIdToLabel[defaultLoaderId]; @@ -411,15 +413,15 @@ export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespac } var { contents, loader = defaultLoader } = result as any; - if ((loader as any) === 'object') { - if (!('exports' in result)) { + if ((loader as any) === "object") { + if (!("exports" in result)) { throw new TypeError('onLoad plugin returning loader: "object" must have "exports" property'); } try { contents = JSON.stringify(result.exports); - loader = 'json'; + loader = "json"; } catch (e) { - throw new TypeError('When using Bun.build, onLoad plugin must return a JSON-serializable object: ' + e) ; + throw new TypeError("When using Bun.build, onLoad plugin must return a JSON-serializable object: " + e); } } diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts index bf1dabf1aa45d1..55145498080c4a 100644 --- a/src/js/builtins/ConsoleObject.ts +++ b/src/js/builtins/ConsoleObject.ts @@ -142,6 +142,9 @@ export function createConsoleConstructor(console: typeof globalThis.console) { const { inspect, formatWithOptions, stripVTControlCharacters } = require("node:util"); const { isBuffer } = require("node:buffer"); + const { validateObject, validateInteger, validateArray } = require("internal/validators"); + const kMaxGroupIndentation = 1000; + const StringPrototypeIncludes = String.prototype.includes; const RegExpPrototypeSymbolReplace = RegExp.prototype[Symbol.replace]; const ArrayPrototypeUnshift = Array.prototype.unshift; @@ -289,28 +292,29 @@ export function createConsoleConstructor(console: typeof globalThis.console) { } = options; if (!stdout || typeof stdout.write !== "function") { - // throw new ERR_CONSOLE_WRITABLE_STREAM("stdout"); - throw new TypeError("stdout is not a writable stream"); + throw $ERR_CONSOLE_WRITABLE_STREAM("stdout is not a writable stream"); } if (!stderr || typeof stderr.write !== "function") { - // throw new ERR_CONSOLE_WRITABLE_STREAM("stderr"); - throw new TypeError("stderr is not a writable stream"); + throw $ERR_CONSOLE_WRITABLE_STREAM("stderr is not a writable stream"); } if (typeof colorMode !== "boolean" && colorMode !== "auto") { - // throw new ERR_INVALID_ARG_VALUE("colorMode", colorMode); - throw new TypeError("colorMode must be a boolean or 'auto'"); + throw $ERR_INVALID_ARG_VALUE( + "The argument 'colorMode' must be one of: 'auto', true, false. Received " + inspect(colorMode), + ); } if (groupIndentation !== undefined) { - // validateInteger(groupIndentation, "groupIndentation", 0, kMaxGroupIndentation); + validateInteger(groupIndentation, "groupIndentation", 0, kMaxGroupIndentation); } if (inspectOptions !== undefined) { - // validateObject(inspectOptions, "options.inspectOptions"); + validateObject(inspectOptions, "options.inspectOptions"); if (inspectOptions.colors !== undefined && options.colorMode !== undefined) { - // throw new ERR_INCOMPATIBLE_OPTION_PAIR("options.inspectOptions.color", "colorMode"); + throw $ERR_INCOMPATIBLE_OPTION_PAIR( + 'Option "options.inspectOptions.color" cannot be used in combination with option "colorMode"', + ); } optionsMap.set(this, inspectOptions); } @@ -455,12 +459,17 @@ export function createConsoleConstructor(console: typeof globalThis.console) { // Add and later remove a noop error handler to catch synchronous // errors. if (stream.listenerCount("error") === 0) stream.once("error", noop); - stream.write(string, errorHandler); } catch (e) { // Console is a debugging utility, so it swallowing errors is not // desirable even in edge cases such as low stack space. - // if (isStackOverflowError(e)) throw e; + if ( + e != null && + typeof e === "object" && + e.name === "RangeError" && + e.message === "Maximum call stack size exceeded." + ) + throw e; // Sorry, there's no proper way to pass along the error here. } finally { stream.removeListener("error", noop); @@ -472,7 +481,7 @@ export function createConsoleConstructor(console: typeof globalThis.console) { value: function (stream) { let color = this[kColorMode]; if (color === "auto") { - if (process.env.FORCE_COLOR !== undefined) { + if (process.env[["FORCE_", "COLOR"].join("")] !== undefined) { color = Bun.enableANSIColors; } else { color = stream.isTTY && (typeof stream.getColorDepth === "function" ? stream.getColorDepth() > 2 : true); @@ -595,7 +604,7 @@ export function createConsoleConstructor(console: typeof globalThis.console) { clear() { // It only makes sense to clear if _stdout is a TTY. // Otherwise, do nothing. - if (this._stdout.isTTY && process.env.TERM !== "dumb") { + if (this._stdout.isTTY && process.env[["TE", "RM"].join("")] !== "dumb") { this._stdout.write("\x1B[2J\x1B[3J\x1B[H"); } }, @@ -643,7 +652,7 @@ export function createConsoleConstructor(console: typeof globalThis.console) { // https://console.spec.whatwg.org/#table table(tabularData, properties) { if (properties !== undefined) { - // validateArray(properties, "properties"); + validateArray(properties, "properties"); } if (tabularData === null || typeof tabularData !== "object") return this.log(tabularData); diff --git a/src/js/internal/primordials.js b/src/js/internal/primordials.js index e68d6d6fe3f6c5..565a056a60de3f 100644 --- a/src/js/internal/primordials.js +++ b/src/js/internal/primordials.js @@ -124,6 +124,7 @@ export default { MathRound: Math.round, MathSqrt: Math.sqrt, MathTrunc: Math.trunc, + MathAbs: Math.abs, Number, NumberIsFinite: Number.isFinite, NumberIsNaN: Number.isNaN, @@ -195,6 +196,7 @@ export default { StringPrototypeSplit: uncurryThis(String.prototype.split), StringPrototypeStartsWith: uncurryThis(String.prototype.startsWith), StringPrototypeToLowerCase: uncurryThis(String.prototype.toLowerCase), + StringPrototypeToUpperCase: uncurryThis(String.prototype.toUpperCase), StringPrototypeTrim: uncurryThis(String.prototype.trim), StringPrototypeValueOf: uncurryThis(String.prototype.valueOf), SymbolPrototypeToString: uncurryThis(Symbol.prototype.toString), diff --git a/src/js/node/events.ts b/src/js/node/events.ts index 462de72833f68a..8e3d875d452a53 100644 --- a/src/js/node/events.ts +++ b/src/js/node/events.ts @@ -341,9 +341,18 @@ EventEmitterPrototype.rawListeners = function rawListeners(type) { return handlers.slice(); }; -EventEmitterPrototype.listenerCount = function listenerCount(type) { +EventEmitterPrototype.listenerCount = function listenerCount(type, method) { var { _events: events } = this; if (!events) return 0; + if (method != null) { + var length = 0; + for (const handler of events[type] ?? []) { + if (handler === method || handler.listener === method) { + length++; + } + } + return length; + } return events[type]?.length ?? 0; }; Object.defineProperty(EventEmitterPrototype.listenerCount, "name", { value: "listenerCount" }); diff --git a/src/js/node/querystring.ts b/src/js/node/querystring.ts index 73a9a0ac15a72a..21d16fb44172b7 100644 --- a/src/js/node/querystring.ts +++ b/src/js/node/querystring.ts @@ -1,398 +1,546 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + var __commonJS = (cb, mod: typeof module | undefined = undefined) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); -var Buffer = require("node:buffer").Buffer; - -// src/node-fallbacks/node_modules/querystring-es3/src/object-keys.js -var require_object_keys = __commonJS((exports, module) => { - var objectKeys = - Object.keys || - (function () { - var hasOwnProperty = Object.prototype.hasOwnProperty; - var hasDontEnumBug = !{ toString: null }.propertyIsEnumerable("toString"); - var dontEnums = [ - "toString", - "toLocaleString", - "valueOf", - "hasOwnProperty", - "isPrototypeOf", - "propertyIsEnumerable", - "constructor", - ]; - var dontEnumsLength = dontEnums.length; - return function (obj) { - if (typeof obj !== "function" && (typeof obj !== "object" || obj === null)) { - throw new TypeError("Object.keys called on non-object"); - } - var result = []; - var prop; - var i; - for (prop in obj) { - if (hasOwnProperty.$call(obj, prop)) { - result.push(prop); - } - } - if (hasDontEnumBug) { - for (i = 0; i < dontEnumsLength; i++) { - if (hasOwnProperty.$call(obj, dontEnums[i])) { - result.push(dontEnums[i]); - } - } - } - return result; - }; - })(); - module.exports = objectKeys; -}); - -// src/node-fallbacks/node_modules/querystring-es3/src/index.js var require_src = __commonJS((exports, module) => { - var ParsedQueryString = function () {}; - var unescapeBuffer = function (s, decodeSpaces) { - var out = Buffer.allocUnsafe(s.length); - var state = 0; - var n, m, hexchar, c; - for (var inIndex = 0, outIndex = 0; ; inIndex++) { - if (inIndex < s.length) { - c = s.charCodeAt(inIndex); - } else { - if (state > 0) { - out[outIndex++] = 37; - if (state === 2) out[outIndex++] = hexchar; + const { + Array, + ArrayIsArray, + Int8Array, + MathAbs, + NumberIsFinite, + ObjectKeys, + String, + StringPrototypeCharCodeAt, + StringPrototypeSlice, + decodeURIComponent, + StringPrototypeToUpperCase, + NumberPrototypeToString, + } = require("internal/primordials"); + + const { Buffer } = require("node:buffer"); + + /** + * @param {string} str + * @param {Int8Array} noEscapeTable + * @param {string[]} hexTable + * @returns {string} + */ + function encodeStr(str, noEscapeTable, hexTable) { + const len = str.length; + if (len === 0) return ""; + + let out = ""; + let lastPos = 0; + let i = 0; + + outer: for (; i < len; i++) { + let c = StringPrototypeCharCodeAt(str, i); + + // ASCII + while (c < 0x80) { + if (noEscapeTable[c] !== 1) { + if (lastPos < i) out += StringPrototypeSlice(str, lastPos, i); + lastPos = i + 1; + out += hexTable[c]; } - break; + + if (++i === len) break outer; + + c = StringPrototypeCharCodeAt(str, i); } - switch (state) { - case 0: - switch (c) { - case 37: - n = 0; - m = 0; - state = 1; - break; - case 43: - if (decodeSpaces) c = 32; - default: - out[outIndex++] = c; - break; - } - break; - case 1: - hexchar = c; - n = unhexTable[c]; - if (!(n >= 0)) { - out[outIndex++] = 37; - out[outIndex++] = c; - state = 0; - break; - } - state = 2; - break; - case 2: - state = 0; - m = unhexTable[c]; - if (!(m >= 0)) { - out[outIndex++] = 37; - out[outIndex++] = hexchar; - out[outIndex++] = c; - break; + + if (lastPos < i) out += StringPrototypeSlice(str, lastPos, i); + + // Multi-byte characters ... + if (c < 0x800) { + lastPos = i + 1; + out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)]; + continue; + } + if (c < 0xd800 || c >= 0xe000) { + lastPos = i + 1; + out += hexTable[0xe0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3f)] + hexTable[0x80 | (c & 0x3f)]; + continue; + } + // Surrogate pair + ++i; + + // This branch should never happen because all URLSearchParams entries + // should already be converted to USVString. But, included for + // completion's sake anyway. + if (i >= len) throw $ERR_INVALID_URI("URI malformed"); + + const c2 = StringPrototypeCharCodeAt(str, i) & 0x3ff; + + lastPos = i + 1; + c = 0x10000 + (((c & 0x3ff) << 10) | c2); + out += + hexTable[0xf0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3f)] + + hexTable[0x80 | ((c >> 6) & 0x3f)] + + hexTable[0x80 | (c & 0x3f)]; + } + if (lastPos === 0) return str; + if (lastPos < len) return out + StringPrototypeSlice(str, lastPos); + return out; + } + + const hexTable = new Array(256); + for (let i = 0; i < 256; ++i) + hexTable[i] = "%" + StringPrototypeToUpperCase((i < 16 ? "0" : "") + NumberPrototypeToString(i, 16)); + // prettier-ignore + const isHexTable = new Int8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64 - 79 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 95 + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96 - 111 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 112 - 127 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ... + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ... 256 + ]); + const QueryString = (module.exports = { + unescapeBuffer, + // `unescape()` is a JS global, so we need to use a different local name + unescape: qsUnescape, + + // `escape()` is a JS global, so we need to use a different local name + escape: qsEscape, + + stringify, + encode: stringify, + + parse, + decode: parse, + }); + + // prettier-ignore + const unhexTable = new Int8Array([ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47 + +0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63 + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95 + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ... + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ... 255 + ]); + /** + * A safe fast alternative to decodeURIComponent + * @param {string} s + * @param {boolean} decodeSpaces + * @returns {string} + */ + function unescapeBuffer(s, decodeSpaces) { + const out = Buffer.allocUnsafe(s.length); + let index = 0; + let outIndex = 0; + let currentChar; + let nextChar; + let hexHigh; + let hexLow; + const maxLength = s.length - 2; + // Flag to know if some hex chars have been decoded + let hasHex = false; + while (index < s.length) { + currentChar = StringPrototypeCharCodeAt(s, index); + if (currentChar === 43 /* '+' */ && decodeSpaces) { + out[outIndex++] = 32; // ' ' + index++; + continue; + } + if (currentChar === 37 /* '%' */ && index < maxLength) { + currentChar = StringPrototypeCharCodeAt(s, ++index); + hexHigh = unhexTable[currentChar]; + if (!(hexHigh >= 0)) { + out[outIndex++] = 37; // '%' + continue; + } else { + nextChar = StringPrototypeCharCodeAt(s, ++index); + hexLow = unhexTable[nextChar]; + if (!(hexLow >= 0)) { + out[outIndex++] = 37; // '%' + index--; + } else { + hasHex = true; + currentChar = hexHigh * 16 + hexLow; } - out[outIndex++] = 16 * n + m; - break; + } } + out[outIndex++] = currentChar; + index++; } - return out.slice(0, outIndex); - }; - var qsUnescape = function (s, decodeSpaces) { + return hasHex ? out.slice(0, outIndex) : out; + } + + /** + * @param {string} s + * @param {boolean} decodeSpaces + * @returns {string} + */ + function qsUnescape(s, decodeSpaces) { try { return decodeURIComponent(s); - } catch (e) { + } catch { return QueryString.unescapeBuffer(s, decodeSpaces).toString(); } - }; - var qsEscape = function (str) { + } + + // These characters do not need escaping when generating query strings: + // ! - . _ ~ + // ' ( ) * + // digits + // alpha (uppercase) + // alpha (lowercase) + // prettier-ignore + const noEscape = new Int8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 112 - 127 +]); + + /** + * QueryString.escape() replaces encodeURIComponent() + * @see https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4 + * @param {any} str + * @returns {string} + */ + function qsEscape(str) { if (typeof str !== "string") { if (typeof str === "object") str = String(str); else str += ""; } - var out = ""; - var lastPos = 0; - for (var i2 = 0; i2 < str.length; ++i2) { - var c = str.charCodeAt(i2); - if (c < 128) { - if (noEscape[c] === 1) continue; - if (lastPos < i2) out += str.slice(lastPos, i2); - lastPos = i2 + 1; - out += hexTable[c]; - continue; - } - if (lastPos < i2) out += str.slice(lastPos, i2); - if (c < 2048) { - lastPos = i2 + 1; - out += hexTable[192 | (c >> 6)] + hexTable[128 | (c & 63)]; - continue; - } - if (c < 55296 || c >= 57344) { - lastPos = i2 + 1; - out += hexTable[224 | (c >> 12)] + hexTable[128 | ((c >> 6) & 63)] + hexTable[128 | (c & 63)]; - continue; - } - ++i2; - var c2; - if (i2 < str.length) c2 = str.charCodeAt(i2) & 1023; - else throw new URIError("URI malformed"); - lastPos = i2 + 1; - c = 65536 + (((c & 1023) << 10) | c2); - out += - hexTable[240 | (c >> 18)] + - hexTable[128 | ((c >> 12) & 63)] + - hexTable[128 | ((c >> 6) & 63)] + - hexTable[128 | (c & 63)]; - } - if (lastPos === 0) return str; - if (lastPos < str.length) return out + str.slice(lastPos); - return out; - }; - var stringifyPrimitive = function (v) { + + return encodeStr(str, noEscape, hexTable); + } + + /** + * @param {string | number | bigint | boolean | symbol | undefined | null} v + * @returns {string} + */ + function stringifyPrimitive(v) { if (typeof v === "string") return v; - if (typeof v === "number" && isFinite(v)) return "" + v; + if (typeof v === "number" && NumberIsFinite(v)) return "" + v; + if (typeof v === "bigint") return "" + v; + if (typeof v === "boolean") return v ? "true" : "false"; + return ""; + } + + /** + * @param {string | number | bigint | boolean} v + * @param {(v: string) => string} encode + * @returns {string} + */ + function encodeStringified(v, encode) { + if (typeof v === "string") return v.length ? encode(v) : ""; + if (typeof v === "number" && NumberIsFinite(v)) { + // Values >= 1e21 automatically switch to scientific notation which requires + // escaping due to the inclusion of a '+' in the output + return MathAbs(v) < 1e21 ? "" + v : encode("" + v); + } + if (typeof v === "bigint") return "" + v; if (typeof v === "boolean") return v ? "true" : "false"; return ""; - }; - var stringify = function (obj, sep, eq, options) { - sep = sep || "&"; - eq = eq || "="; - var encode = QueryString.escape; + } + + /** + * @param {string | number | boolean | null} v + * @param {(v: string) => string} encode + * @returns {string} + */ + function encodeStringifiedCustom(v, encode) { + return encode(stringifyPrimitive(v)); + } + + /** + * @param {Record | null>} obj + * @param {string} [sep] + * @param {string} [eq] + * @param {{ encodeURIComponent?: (v: string) => string }} [options] + * @returns {string} + */ + function stringify(obj, sep, eq, options) { + sep ||= "&"; + eq ||= "="; + + let encode = QueryString.escape; if (options && typeof options.encodeURIComponent === "function") { encode = options.encodeURIComponent; } + const convert = encode === qsEscape ? encodeStringified : encodeStringifiedCustom; + if (obj !== null && typeof obj === "object") { - var keys = objectKeys(obj); - var len = keys.length; - var flast = len - 1; - var fields = ""; - for (var i2 = 0; i2 < len; ++i2) { - var k = keys[i2]; - var v = obj[k]; - var ks = encode(stringifyPrimitive(k)) + eq; - if (isArray(v)) { - var vlen = v.length; - var vlast = vlen - 1; - for (var j = 0; j < vlen; ++j) { - fields += ks + encode(stringifyPrimitive(v[j])); - if (j < vlast) fields += sep; + const keys = ObjectKeys(obj); + const len = keys.length; + let fields = ""; + for (let i = 0; i < len; ++i) { + const k = keys[i]; + const v = obj[k]; + let ks = convert(k, encode); + ks += eq; + + if (ArrayIsArray(v)) { + const vlen = v.length; + if (vlen === 0) continue; + if (fields) fields += sep; + for (let j = 0; j < vlen; ++j) { + if (j) fields += sep; + fields += ks; + fields += convert(v[j], encode); } - if (vlen && i2 < flast) fields += sep; } else { - fields += ks + encode(stringifyPrimitive(v)); - if (i2 < flast) fields += sep; + if (fields) fields += sep; + fields += ks; + fields += convert(v, encode); } } return fields; } return ""; - }; - var charCodes = function (str) { + } + + /** + * @param {string} str + * @returns {number[]} + */ + function charCodes(str) { if (str.length === 0) return []; - if (str.length === 1) return [str.charCodeAt(0)]; - const ret = []; - for (var i2 = 0; i2 < str.length; ++i2) ret[ret.length] = str.charCodeAt(i2); + if (str.length === 1) return [StringPrototypeCharCodeAt(str, 0)]; + const ret = new Array(str.length); + for (let i = 0; i < str.length; ++i) ret[i] = StringPrototypeCharCodeAt(str, i); return ret; - }; - var parse = function (qs, sep, eq, options) { - const obj = new ParsedQueryString(); + } + const defSepCodes = [38]; // & + const defEqCodes = [61]; // = + + function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) { + if (key.length > 0 && keyEncoded) key = decodeStr(key, decode); + if (value.length > 0 && valEncoded) value = decodeStr(value, decode); + + if (obj[key] === undefined) { + obj[key] = value; + } else { + const curValue = obj[key]; + // A simple Array-specific property check is enough here to + // distinguish from a string value and is faster and still safe + // since we are generating all of the values being assigned. + if (curValue.pop) curValue[curValue.length] = value; + else obj[key] = [curValue, value]; + } + } + + /** + * Parse a key/val string. + * @param {string} qs + * @param {string} sep + * @param {string} eq + * @param {{ + * maxKeys?: number; + * decodeURIComponent?(v: string): string; + * }} [options] + * @returns {Record} + */ + function parse(qs, sep, eq, options) { + const obj = { __proto__: null }; + if (typeof qs !== "string" || qs.length === 0) { return obj; } - var sepCodes = !sep ? defSepCodes : charCodes(sep + ""); - var eqCodes = !eq ? defEqCodes : charCodes(eq + ""); + + const sepCodes = !sep ? defSepCodes : charCodes(String(sep)); + const eqCodes = !eq ? defEqCodes : charCodes(String(eq)); const sepLen = sepCodes.length; const eqLen = eqCodes.length; - var pairs = 1000; + + let pairs = 1000; if (options && typeof options.maxKeys === "number") { + // -1 is used in place of a value like Infinity for meaning + // "unlimited pairs" because of additional checks V8 (at least as of v5.4) + // has to do when using variables that contain values like Infinity. Since + // `pairs` is always decremented and checked explicitly for 0, -1 works + // effectively the same as Infinity, while providing a significant + // performance boost. pairs = options.maxKeys > 0 ? options.maxKeys : -1; } - var decode = QueryString.unescape; + + let decode = QueryString.unescape; if (options && typeof options.decodeURIComponent === "function") { decode = options.decodeURIComponent; } const customDecode = decode !== qsUnescape; - const keys = []; - var posIdx = 0; - var lastPos = 0; - var sepIdx = 0; - var eqIdx = 0; - var key = ""; - var value = ""; - var keyEncoded = customDecode; - var valEncoded = customDecode; - var encodeCheck = 0; - for (var i2 = 0; i2 < qs.length; ++i2) { - const code = qs.charCodeAt(i2); + + let lastPos = 0; + let sepIdx = 0; + let eqIdx = 0; + let key = ""; + let value = ""; + let keyEncoded = customDecode; + let valEncoded = customDecode; + const plusChar = customDecode ? "%20" : " "; + let encodeCheck = 0; + for (let i = 0; i < qs.length; ++i) { + const code = StringPrototypeCharCodeAt(qs, i); + + // Try matching key/value pair separator (e.g. '&') if (code === sepCodes[sepIdx]) { if (++sepIdx === sepLen) { - const end = i2 - sepIdx + 1; + // Key/value pair separator match! + const end = i - sepIdx + 1; if (eqIdx < eqLen) { - if (lastPos < end) key += qs.slice(lastPos, end); - } else if (lastPos < end) value += qs.slice(lastPos, end); - if (keyEncoded) key = decodeStr(key, decode); - if (valEncoded) value = decodeStr(value, decode); - if (key || value || lastPos - posIdx > sepLen || i2 === 0) { - if (indexOf(keys, key) === -1) { - obj[key] = value; - keys[keys.length] = key; - } else { - const curValue = obj[key] || ""; - if (curValue.pop) curValue[curValue.length] = value; - else if (curValue) obj[key] = [curValue, value]; + // We didn't find the (entire) key/value separator + if (lastPos < end) { + // Treat the substring as part of the key instead of the value + key += StringPrototypeSlice(qs, lastPos, end); + } else if (key.length === 0) { + // We saw an empty substring between separators + if (--pairs === 0) return obj; + lastPos = i + 1; + sepIdx = eqIdx = 0; + continue; } - } else if (i2 === 1) { - delete obj[key]; + } else if (lastPos < end) { + value += StringPrototypeSlice(qs, lastPos, end); } - if (--pairs === 0) break; + + addKeyVal(obj, key, value, keyEncoded, valEncoded, decode); + + if (--pairs === 0) return obj; keyEncoded = valEncoded = customDecode; - encodeCheck = 0; key = value = ""; - posIdx = lastPos; - lastPos = i2 + 1; + encodeCheck = 0; + lastPos = i + 1; sepIdx = eqIdx = 0; } - continue; } else { sepIdx = 0; - if (!valEncoded) { - if (code === 37) { - encodeCheck = 1; - } else if ( - encodeCheck > 0 && - ((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102)) - ) { - if (++encodeCheck === 3) valEncoded = true; + // Try matching key/value separator (e.g. '=') if we haven't already + if (eqIdx < eqLen) { + if (code === eqCodes[eqIdx]) { + if (++eqIdx === eqLen) { + // Key/value separator match! + const end = i - eqIdx + 1; + if (lastPos < end) key += StringPrototypeSlice(qs, lastPos, end); + encodeCheck = 0; + lastPos = i + 1; + } + continue; } else { - encodeCheck = 0; + eqIdx = 0; + if (!keyEncoded) { + // Try to match an (valid) encoded byte once to minimize unnecessary + // calls to string decoding functions + if (code === 37 /* % */) { + encodeCheck = 1; + continue; + } else if (encodeCheck > 0) { + if (isHexTable[code] === 1) { + if (++encodeCheck === 3) keyEncoded = true; + continue; + } else { + encodeCheck = 0; + } + } + } } - } - } - if (eqIdx < eqLen) { - if (code === eqCodes[eqIdx]) { - if (++eqIdx === eqLen) { - const end = i2 - eqIdx + 1; - if (lastPos < end) key += qs.slice(lastPos, end); - encodeCheck = 0; - lastPos = i2 + 1; + if (code === 43 /* + */) { + if (lastPos < i) key += StringPrototypeSlice(qs, lastPos, i); + key += plusChar; + lastPos = i + 1; + continue; } - continue; - } else { - eqIdx = 0; - if (!keyEncoded) { - if (code === 37) { - encodeCheck = 1; - } else if ( - encodeCheck > 0 && - ((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102)) - ) { - if (++encodeCheck === 3) keyEncoded = true; + } + if (code === 43 /* + */) { + if (lastPos < i) value += StringPrototypeSlice(qs, lastPos, i); + value += plusChar; + lastPos = i + 1; + } else if (!valEncoded) { + // Try to match an (valid) encoded byte (once) to minimize unnecessary + // calls to string decoding functions + if (code === 37 /* % */) { + encodeCheck = 1; + } else if (encodeCheck > 0) { + if (isHexTable[code] === 1) { + if (++encodeCheck === 3) valEncoded = true; } else { encodeCheck = 0; } } } } - if (code === 43) { - if (eqIdx < eqLen) { - if (lastPos < i2) key += qs.slice(lastPos, i2); - key += "%20"; - keyEncoded = true; - } else { - if (lastPos < i2) value += qs.slice(lastPos, i2); - value += "%20"; - valEncoded = true; - } - lastPos = i2 + 1; - } } - if (pairs !== 0 && (lastPos < qs.length || eqIdx > 0)) { - if (lastPos < qs.length) { - if (eqIdx < eqLen) key += qs.slice(lastPos); - else if (sepIdx < sepLen) value += qs.slice(lastPos); - } - if (keyEncoded) key = decodeStr(key, decode); - if (valEncoded) value = decodeStr(value, decode); - if (indexOf(keys, key) === -1) { - obj[key] = value; - keys[keys.length] = key; - } else { - const curValue = obj[key]; - if (curValue.pop) curValue[curValue.length] = value; - else obj[key] = [curValue, value]; - } + + // Deal with any leftover key or value data + if (lastPos < qs.length) { + if (eqIdx < eqLen) key += StringPrototypeSlice(qs, lastPos); + else if (sepIdx < sepLen) value += StringPrototypeSlice(qs, lastPos); + } else if (eqIdx === 0 && key.length === 0) { + // We ended on an empty substring + return obj; } + + addKeyVal(obj, key, value, keyEncoded, valEncoded, decode); + return obj; - }; - var decodeStr = function (s, decoder) { + } + + /** + * V8 does not optimize functions with try-catch blocks, so we isolate them here + * to minimize the damage (Note: no longer true as of V8 5.4 -- but still will + * not be inlined). + * @param {string} s + * @param {(v: string) => string} decoder + * @returns {string} + */ + function decodeStr(s, decoder) { try { return decoder(s); - } catch (e) { + } catch { return QueryString.unescape(s, true); } - }; - var QueryString = (module.exports = { - unescapeBuffer, - unescape: qsUnescape, - escape: qsEscape, - stringify, - encode: stringify, - parse, - decode: parse, - }); - var objectKeys = require_object_keys(); - var isArray = arg => Object.prototype.toString.$call(arg) === "[object Array]"; - var indexOf = (arr, searchElement, fromIndex) => { - var k; - if (arr == null) { - throw new TypeError('"arr" is null or not defined'); - } - var o = Object(arr); - var len = o.length >>> 0; - if (len === 0) { - return -1; - } - var n = fromIndex | 0; - if (n >= len) { - return -1; - } - k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); - while (k < len) { - if (k in o && o[k] === searchElement) { - return k; - } - k++; - } - return -1; - }; - ParsedQueryString.prototype = Object.create ? Object.create(null) : {}; - var unhexTable = [ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, - -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - ]; - var hexTable = []; - for (i = 0; i < 256; ++i) hexTable[i] = "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase(); - var i; - var noEscape = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, - 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, - ]; - var defSepCodes = [38]; - var defEqCodes = [61]; + } }); export default require_src(); diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index ec729b3da6577f..38b89765205d95 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -3574,31 +3574,16 @@ pub const Interpreter = struct { pipe[0] = bun.FDImpl.fromUV(fds[0]).encode(); pipe[1] = bun.FDImpl.fromUV(fds[1]).encode(); } else { - const fds: [2]bun.FileDescriptor = brk: { - var fds_: [2]std.c.fd_t = undefined; - const rc = std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &fds_); - if (rc != 0) { - return bun.sys.Maybe(void).errno(bun.sys.getErrno(rc), .socketpair); - } - - var before = std.c.fcntl(fds_[0], std.posix.F.GETFL); - - const result = std.c.fcntl(fds_[0], std.posix.F.SETFL, before | bun.O.CLOEXEC); - if (result == -1) { - _ = bun.sys.close(bun.toFD(fds_[0])); - _ = bun.sys.close(bun.toFD(fds_[1])); - return Maybe(void).errno(bun.sys.getErrno(result), .fcntl); - } - - if (comptime bun.Environment.isMac) { - // SO_NOSIGPIPE - before = 1; - _ = std.c.setsockopt(fds_[0], std.posix.SOL.SOCKET, std.posix.SO.NOSIGPIPE, &before, @sizeOf(c_int)); - } - - break :brk .{ bun.toFD(fds_[0]), bun.toFD(fds_[1]) }; + const fds: [2]bun.FileDescriptor = switch (bun.sys.socketpair( + std.posix.AF.UNIX, + std.posix.SOCK.STREAM, + 0, + .blocking, + )) { + .result => |fds| .{ bun.toFD(fds[0]), bun.toFD(fds[1]) }, + .err => |err| return .{ .err = err }, }; - log("socketpair() = {{{}, {}}}", .{ fds[0], fds[1] }); + pipe.* = fds; } set_count.* += 1; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 2b1f084d04b6a9..286ac906b768f1 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1559,20 +1559,14 @@ pub fn nonASCIISequenceLength(first_byte: u8) u3 { pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime sentinel: bool) !if (sentinel) ?[:0]u16 else ?[]u16 { if (strings.firstNonASCII(bytes)) |i| { const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { - const trimmed = bun.simdutf.trim.utf8(bytes); - - if (trimmed.len == 0) - break :simd null; - - const out_length = bun.simdutf.length.utf16.from.utf8(trimmed); - + const out_length = bun.simdutf.length.utf16.from.utf8(bytes); if (out_length == 0) break :simd null; var out = try allocator.alloc(u16, out_length + if (sentinel) 1 else 0); log("toUTF16 {d} UTF8 -> {d} UTF16", .{ bytes.len, out_length }); - const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(trimmed, out); + const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(bytes, out); if (res.status == .success) { if (comptime sentinel) { out[out_length] = 0; @@ -1778,104 +1772,6 @@ pub fn toUTF16AllocMaybeBuffered( return .{ output.items, .{0} ** 3, 0 }; } -pub fn toUTF16AllocNoTrim(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime _: bool) !?[]u16 { - if (strings.firstNonASCII(bytes)) |i| { - const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { - const out_length = bun.simdutf.length.utf16.from.utf8(bytes); - - if (out_length == 0) - break :simd null; - - var out = try allocator.alloc(u16, out_length); - log("toUTF16 {d} UTF8 -> {d} UTF16", .{ bytes.len, out_length }); - - const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(bytes, out); - if (res.status == .success) { - return out; - } - - if (comptime fail_if_invalid) { - allocator.free(out); - return error.InvalidByteSequence; - } - - break :simd .{ - .items = out[0..i], - .capacity = out.len, - .allocator = allocator, - }; - } else null; - var output = output_ orelse fallback: { - var list = try std.ArrayList(u16).initCapacity(allocator, i + 2); - list.items.len = i; - strings.copyU8IntoU16(list.items, bytes[0..i]); - break :fallback list; - }; - errdefer output.deinit(); - - var remaining = bytes[i..]; - - { - const replacement = strings.convertUTF8BytesIntoUTF16(remaining); - if (comptime fail_if_invalid) { - if (replacement.fail) { - if (comptime Environment.allow_assert) assert(replacement.code_point == unicode_replacement); - return error.InvalidByteSequence; - } - } - remaining = remaining[@max(replacement.len, 1)..]; - - //#define U16_LENGTH(c) ((uint32_t)(c)<=0xffff ? 1 : 2) - switch (replacement.code_point) { - 0...0xffff => |c| { - try output.append(@as(u16, @intCast(c))); - }, - else => |c| { - try output.appendSlice(&[_]u16{ strings.u16Lead(c), strings.u16Trail(c) }); - }, - } - } - - while (strings.firstNonASCII(remaining)) |j| { - const end = output.items.len; - try output.ensureUnusedCapacity(j); - output.items.len += j; - strings.copyU8IntoU16(output.items[end..][0..j], remaining[0..j]); - remaining = remaining[j..]; - - const replacement = strings.convertUTF8BytesIntoUTF16(remaining); - if (comptime fail_if_invalid) { - if (replacement.fail) { - if (comptime Environment.allow_assert) assert(replacement.code_point == unicode_replacement); - return error.InvalidByteSequence; - } - } - remaining = remaining[@max(replacement.len, 1)..]; - - //#define U16_LENGTH(c) ((uint32_t)(c)<=0xffff ? 1 : 2) - switch (replacement.code_point) { - 0...0xffff => |c| { - try output.append(@as(u16, @intCast(c))); - }, - else => |c| { - try output.appendSlice(&[_]u16{ strings.u16Lead(c), strings.u16Trail(c) }); - }, - } - } - - if (remaining.len > 0) { - try output.ensureTotalCapacityPrecise(output.items.len + remaining.len); - - output.items.len += remaining.len; - strings.copyU8IntoU16(output.items[output.items.len - remaining.len ..], remaining); - } - - return output.items; - } - - return null; -} - pub fn utf16CodepointWithFFFD(comptime Type: type, input: Type) UTF16Replacement { return utf16CodepointWithFFFDAndFirstInputChar(Type, input[0], input); } diff --git a/src/sys.zig b/src/sys.zig index 6cc5e3dcaf2dd6..1bc3663159af07 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -249,6 +249,7 @@ pub const Tag = enum(u8) { pipe, try_write, socketpair, + setsockopt, uv_spawn, uv_pipe, @@ -751,9 +752,16 @@ pub fn mkdirOSPath(file_path: bun.OSPathSliceZ, flags: bun.Mode) Maybe(void) { const fnctl_int = if (Environment.isLinux) usize else c_int; pub fn fcntl(fd: bun.FileDescriptor, cmd: i32, arg: fnctl_int) Maybe(fnctl_int) { - const result = fcntl_symbol(fd.cast(), cmd, arg); - if (Maybe(fnctl_int).errnoSys(result, .fcntl)) |err| return err; - return .{ .result = @intCast(result) }; + while (true) { + const result = fcntl_symbol(fd.cast(), cmd, arg); + if (Maybe(fnctl_int).errnoSys(result, .fcntl)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + return .{ .result = @intCast(result) }; + } + + unreachable; } pub fn getErrno(rc: anytype) bun.C.E { @@ -2323,6 +2331,132 @@ pub fn mmapFile(path: [:0]const u8, flags: std.c.MAP, wanted_size: ?usize, offse return .{ .result = map }; } +pub fn setCloseOnExec(fd: bun.FileDescriptor) Maybe(void) { + switch (fcntl(fd, std.posix.F.GETFD, 0)) { + .result => |fl| { + switch (fcntl(fd, std.posix.F.SETFD, fl | std.posix.FD_CLOEXEC)) { + .result => {}, + .err => |err| return .{ .err = err }, + } + }, + .err => |err| return .{ .err = err }, + } + + return .{ .result = {} }; +} + +pub fn setsockopt(fd: bun.FileDescriptor, level: c_int, optname: u32, value: i32) Maybe(i32) { + while (true) { + const rc = syscall.setsockopt(fd.cast(), level, optname, &value, @sizeOf(i32)); + if (Maybe(i32).errnoSys(rc, .setsockopt)) |err| { + if (err.getErrno() == .INTR) continue; + log("setsockopt() = {d} {s}", .{ err.err.errno, err.err.name() }); + return err; + } + log("setsockopt({d}, {d}, {d}) = {d}", .{ fd.cast(), level, optname, rc }); + return .{ .result = @intCast(rc) }; + } + + unreachable; +} + +pub fn setNoSigpipe(fd: bun.FileDescriptor) Maybe(void) { + if (comptime Environment.isMac) { + return switch (setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.NOSIGPIPE, 1)) { + .result => .{ .result = {} }, + .err => |err| .{ .err = err }, + }; + } + + return .{ .result = {} }; +} + +const socketpair_t = if (Environment.isLinux) i32 else c_uint; + +/// libc socketpair() except it defaults to: +/// - SOCK_CLOEXEC on Linux +/// - SO_NOSIGPIPE on macOS +/// +/// On POSIX it otherwise makes it do O_CLOEXEC. +pub fn socketpair(domain: socketpair_t, socktype: socketpair_t, protocol: socketpair_t, nonblocking_status: enum { blocking, nonblocking }) Maybe([2]bun.FileDescriptor) { + if (comptime !Environment.isPosix) @compileError("linux only!"); + + var fds_i: [2]syscall.fd_t = .{ 0, 0 }; + + if (comptime Environment.isLinux) { + while (true) { + const nonblock_flag: i32 = if (nonblocking_status == .nonblocking) linux.SOCK.NONBLOCK else 0; + const rc = std.os.linux.socketpair(domain, socktype | linux.SOCK.CLOEXEC | nonblock_flag, protocol, &fds_i); + if (Maybe([2]bun.FileDescriptor).errnoSys(rc, .socketpair)) |err| { + if (err.getErrno() == .INTR) continue; + + log("socketpair() = {d} {s}", .{ err.err.errno, err.err.name() }); + return err; + } + + break; + } + } else { + while (true) { + const err = libc.socketpair(domain, socktype, protocol, &fds_i); + + if (Maybe([2]bun.FileDescriptor).errnoSys(err, .socketpair)) |err2| { + if (err2.getErrno() == .INTR) continue; + log("socketpair() = {d} {s}", .{ err2.err.errno, err2.err.name() }); + return err2; + } + + break; + } + + const err: ?Syscall.Error = err: { + + // Set O_CLOEXEC first. + inline for (0..2) |i| { + switch (setCloseOnExec(bun.toFD(fds_i[i]))) { + .err => |err| break :err err, + .result => {}, + } + } + + if (comptime Environment.isMac) { + inline for (0..2) |i| { + switch (setNoSigpipe(bun.toFD(fds_i[i]))) { + .err => |err| break :err err, + else => {}, + } + } + } + + if (nonblocking_status == .nonblocking) { + inline for (0..2) |i| { + switch (setNonblocking(bun.toFD(fds_i[i]))) { + .err => |err| break :err err, + .result => {}, + } + } + } + + break :err null; + }; + + // On any error after socketpair(), we need to close it. + if (err) |errr| { + inline for (0..2) |i| { + _ = close(bun.toFD(fds_i[i])); + } + + log("socketpair() = {d} {s}", .{ errr.errno, errr.name() }); + + return .{ .err = errr }; + } + } + + log("socketpair() = [{d} {d}]", .{ fds_i[0], fds_i[1] }); + + return Maybe([2]bun.FileDescriptor){ .result = .{ bun.toFD(fds_i[0]), bun.toFD(fds_i[1]) } }; +} + pub fn munmap(memory: []align(mem.page_size) const u8) Maybe(void) { if (Maybe(void).errnoSys(syscall.munmap(memory.ptr, memory.len), .munmap)) |err| { return err; diff --git a/test/bundler/native-plugin.test.ts b/test/bundler/native-plugin.test.ts index 83ef6acaaf38a0..10c67d8ce56a3e 100644 --- a/test/bundler/native-plugin.test.ts +++ b/test/bundler/native-plugin.test.ts @@ -2,9 +2,12 @@ import { BunFile, Loader, plugin } from "bun"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; import path, { dirname, join, resolve } from "path"; import source from "./native_plugin.cc" with { type: "file" }; +import notAPlugin from "./not_native_plugin.cc" with { type: "file" }; import bundlerPluginHeader from "../../packages/bun-native-bundler-plugin-api/bundler_plugin.h" with { type: "file" }; -import { bunEnv, bunExe, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, makeTree, tempDirWithFiles } from "harness"; import { itBundled } from "bundler/expectBundled"; +import os from "os"; +import fs from "fs"; describe("native-plugins", async () => { const cwd = process.cwd(); @@ -15,6 +18,7 @@ describe("native-plugins", async () => { const files = { "bun-native-bundler-plugin-api/bundler_plugin.h": await Bun.file(bundlerPluginHeader).text(), "plugin.cc": await Bun.file(source).text(), + "not_a_plugin.cc": await Bun.file(notAPlugin).text(), "package.json": JSON.stringify({ "name": "fake-plugin", "module": "index.ts", @@ -48,12 +52,19 @@ values;`, "target_name": "xXx123_foo_counter_321xXx", "sources": [ "plugin.cc" ], "include_dirs": [ "." ] + }, + { + "target_name": "not_a_plugin", + "sources": [ "not_a_plugin.cc" ], + "include_dirs": [ "." ] } ] }`, }; tempdir = tempDirWithFiles("native-plugins", files); + + await makeTree(tempdir, files); outdir = path.join(tempdir, "dist"); console.log("tempdir", tempdir); @@ -491,6 +502,54 @@ const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] expect(compilationCtxFreedCount).toBe(0); }); + it("should fail gracefully when passing something that is NOT a bunler plugin", async () => { + const not_plugins = [require(path.join(tempdir, "build/Release/not_a_plugin.node")), 420, "hi", {}]; + + for (const napiModule of not_plugins) { + try { + await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "not_a_plugin", + setup(build) { + build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "plugin_impl" }); + }, + }, + ], + }); + expect.unreachable(); + } catch (e) { + expect(e.toString()).toContain( + "onBeforeParse `napiModule` must be a Napi module which exports the `BUN_PLUGIN_NAME` symbol.", + ); + } + } + }); + + it("should fail gracefully when can't find the symbol", async () => { + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + + try { + await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "not_a_plugin", + setup(build) { + build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "OOGA_BOOGA_420" }); + }, + }, + ], + }); + expect.unreachable(); + } catch (e) { + expect(e.toString()).toContain('TypeError: Could not find the symbol "OOGA_BOOGA_420" in the given napi module.'); + } + }); + it("should use result of the first plugin that runs and doesn't execute the others", async () => { const filter = /\.ts/; diff --git a/test/bundler/native_plugin.cc b/test/bundler/native_plugin.cc index b48eec7dacfb83..51b13fd07d94d7 100644 --- a/test/bundler/native_plugin.cc +++ b/test/bundler/native_plugin.cc @@ -19,7 +19,7 @@ #include #endif -BUN_PLUGIN_EXPORT const char *BUN_PLUGIN_NAME = "native_plugin_test"; +extern "C" BUN_PLUGIN_EXPORT const char *BUN_PLUGIN_NAME = "native_plugin_test"; struct External { std::atomic foo_count; diff --git a/test/bundler/not_native_plugin.cc b/test/bundler/not_native_plugin.cc new file mode 100644 index 00000000000000..1de24320d939ed --- /dev/null +++ b/test/bundler/not_native_plugin.cc @@ -0,0 +1,27 @@ +/* + */ +#include +#include +#include +#include + +#ifdef _WIN32 +#define BUN_PLUGIN_EXPORT __declspec(dllexport) +#else +#define BUN_PLUGIN_EXPORT +#endif + +napi_value HelloWorld(napi_env env, napi_callback_info info) { + napi_value result; + napi_create_string_utf8(env, "hello world", NAPI_AUTO_LENGTH, &result); + return result; +} + +napi_value Init(napi_env env, napi_value exports) { + napi_value fn; + napi_create_function(env, nullptr, 0, HelloWorld, nullptr, &fn); + napi_set_named_property(env, exports, "helloWorld", fn); + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/harness.ts b/test/harness.ts index 0921b1dcc01b97..eb55b7b6820454 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -152,25 +152,29 @@ export type DirectoryTree = { | ((opts: { root: string }) => Awaitable); }; -export function tempDirWithFiles(basename: string, files: DirectoryTree): string { - async function makeTree(base: string, tree: DirectoryTree) { - for (const [name, raw_contents] of Object.entries(tree)) { - const contents = typeof raw_contents === "function" ? await raw_contents({ root: base }) : raw_contents; - const joined = join(base, name); - if (name.includes("/")) { - const dir = dirname(name); - if (dir !== name && dir !== ".") { - fs.mkdirSync(join(base, dir), { recursive: true }); - } +export async function makeTree(base: string, tree: DirectoryTree) { + const isDirectoryTree = (value: string | DirectoryTree | Buffer): value is DirectoryTree => + typeof value === "object" && value && typeof value?.byteLength === "undefined"; + + for (const [name, raw_contents] of Object.entries(tree)) { + const contents = typeof raw_contents === "function" ? await raw_contents({ root: base }) : raw_contents; + const joined = join(base, name); + if (name.includes("/")) { + const dir = dirname(name); + if (dir !== name && dir !== ".") { + fs.mkdirSync(join(base, dir), { recursive: true }); } - if (typeof contents === "object" && contents && typeof contents?.byteLength === "undefined") { - fs.mkdirSync(joined); - makeTree(joined, contents); - continue; - } - fs.writeFileSync(joined, contents); } + if (isDirectoryTree(contents)) { + fs.mkdirSync(joined); + makeTree(joined, contents); + continue; + } + fs.writeFileSync(joined, contents); } +} + +export function tempDirWithFiles(basename: string, files: DirectoryTree): string { const base = fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()), basename + "_")); makeTree(base, files); return base; diff --git a/test/js/bun/css/css-fuzz.test.ts b/test/js/bun/css/css-fuzz.test.ts index 6a46bfc16b4540..668847af94c754 100644 --- a/test/js/bun/css/css-fuzz.test.ts +++ b/test/js/bun/css/css-fuzz.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; -import { isCI } from "harness"; +import { isCI, isDebug } from "harness"; interface InvalidFuzzOptions { maxLength: number; @@ -7,6 +7,9 @@ interface InvalidFuzzOptions { iterations: number; } +const shutup = process.env.CSS_FUZZ_SHUTUP === "1"; +const log = shutup ? () => {} : console.log; + // Collection of invalid CSS generation strategies const invalidGenerators = { // Syntax errors @@ -62,7 +65,7 @@ const invalidGenerators = { // Memory and resource stress memory: { - deepNesting: (depth: number = 1000) => { + deepNesting: (depth: number = 300) => { let css = ""; for (let i = 0; i < depth; i++) { css += "@media screen {"; @@ -111,107 +114,108 @@ function corruptCSS(css: string): string { // TODO: if (!isCI) { // Main fuzzing test suite for invalid inputs - test.each([ - ["syntax", 1000], - ["structure", 1000], - ["encoding", 500], - ["memory", 100], - ])("CSS Parser Invalid Input Fuzzing - %s (%d iterations)", async (strategy, iterations) => { - const options: InvalidFuzzOptions = { - maxLength: 10000, - strategy: strategy as any, - iterations, - }; - - let crashCount = 0; - let errorCount = 0; - const startTime = performance.now(); - - for (let i = 0; i < options.iterations; i++) { - let invalidCSS = ""; - - switch (strategy) { - case "syntax": - invalidCSS = - invalidGenerators.syntax[ - Object.keys(invalidGenerators.syntax)[ - Math.floor(Math.random() * Object.keys(invalidGenerators.syntax).length) - ] - ]()[Math.floor(Math.random() * 5)]; - break; - - case "structure": - invalidCSS = - invalidGenerators.structure[ - Object.keys(invalidGenerators.structure)[ - Math.floor(Math.random() * Object.keys(invalidGenerators.structure).length) - ] - ]()[Math.floor(Math.random() * 3)]; - break; - - case "encoding": - invalidCSS = - invalidGenerators.encoding[ - Object.keys(invalidGenerators.encoding)[ - Math.floor(Math.random() * Object.keys(invalidGenerators.encoding).length) - ] - ]()[0]; - break; - - case "memory": - const memoryFuncs = Object.keys(invalidGenerators.memory); - const selectedFunc = memoryFuncs[Math.floor(Math.random() * memoryFuncs.length)]; - invalidCSS = invalidGenerators.memory[selectedFunc](1000); - break; - } + test.each( + [["syntax", 1000], ["structure", 1000], ["encoding", 500], !isDebug ? ["memory", 100] : []].filter( + xs => xs.length > 0, + ), + )( + "CSS Parser Invalid Input Fuzzing - %s (%d iterations)", + async (strategy, iterations) => { + const options: InvalidFuzzOptions = { + maxLength: 10000, + strategy: strategy as any, + iterations, + }; - // Further corrupt the CSS randomly - if (Math.random() < 0.3) { - invalidCSS = corruptCSS(invalidCSS); - } + let crashCount = 0; + let errorCount = 0; + const startTime = performance.now(); - console.log("--- CSS Fuzz ---"); - invalidCSS = invalidCSS + ""; - console.log(JSON.stringify(invalidCSS, null, 2)); - await Bun.write("invalid.css", invalidCSS); + for (let i = 0; i < options.iterations; i++) { + let invalidCSS = ""; - try { - const result = await Bun.build({ - entrypoints: ["invalid.css"], - experimentalCss: true, - }); + switch (strategy) { + case "syntax": + invalidCSS = + invalidGenerators.syntax[ + Object.keys(invalidGenerators.syntax)[ + Math.floor(Math.random() * Object.keys(invalidGenerators.syntax).length) + ] + ]()[Math.floor(Math.random() * 5)]; + break; + + case "structure": + invalidCSS = + invalidGenerators.structure[ + Object.keys(invalidGenerators.structure)[ + Math.floor(Math.random() * Object.keys(invalidGenerators.structure).length) + ] + ]()[Math.floor(Math.random() * 3)]; + break; - if (result.logs.length > 0) { - throw new AggregateError("CSS parser returned logs", result.logs); + case "encoding": + invalidCSS = + invalidGenerators.encoding[ + Object.keys(invalidGenerators.encoding)[ + Math.floor(Math.random() * Object.keys(invalidGenerators.encoding).length) + ] + ]()[0]; + break; + + case "memory": + const memoryFuncs = Object.keys(invalidGenerators.memory); + const selectedFunc = memoryFuncs[Math.floor(Math.random() * memoryFuncs.length)]; + invalidCSS = invalidGenerators.memory[selectedFunc](); + break; } - // We expect the parser to either throw an error or return a valid result - // If it returns undefined/null, that's a potential issue - if (result === undefined || result === null) { - crashCount++; - console.error(`Parser returned ${result} for input:\n${invalidCSS.slice(0, 100)}...`); + // Further corrupt the CSS randomly + if (Math.random() < 0.3) { + invalidCSS = corruptCSS(invalidCSS); } - } catch (error) { - // Expected behavior for invalid CSS - errorCount++; - // Check for specific error types we want to track - if (error instanceof RangeError || error instanceof TypeError) { - console.warn(`Unexpected error type: ${error.constructor.name} for input:\n${invalidCSS.slice(0, 100)}...`); + log("--- CSS Fuzz ---"); + invalidCSS = invalidCSS + ""; + log(JSON.stringify(invalidCSS, null, 2)); + await Bun.write("invalid.css", invalidCSS); + + try { + const result = await Bun.build({ + entrypoints: ["invalid.css"], + experimentalCss: true, + }); + + if (result.logs.length > 0) { + throw new AggregateError("CSS parser returned logs", result.logs); + } + + // We expect the parser to either throw an error or return a valid result + // If it returns undefined/null, that's a potential issue + if (result === undefined || result === null) { + crashCount++; + console.error(`Parser returned ${result} for input:\n${invalidCSS.slice(0, 100)}...`); + } + } catch (error) { + // Expected behavior for invalid CSS + errorCount++; + + // Check for specific error types we want to track + if (error instanceof RangeError || error instanceof TypeError) { + console.warn(`Unexpected error type: ${error.constructor.name} for input:\n${invalidCSS.slice(0, 100)}...`); + } } - } - // Memory check every 100 iterations - if (i % 100 === 0) { - const heapUsed = process.memoryUsage().heapUsed / 1024 / 1024; - expect(heapUsed).toBeLessThan(500); // Alert if memory usage exceeds 500MB + // Memory check every 100 iterations + if (i % 100 === 0) { + const heapUsed = process.memoryUsage().heapUsed / 1024 / 1024; + expect(heapUsed).toBeLessThan(500); // Alert if memory usage exceeds 500MB + } } - } - const endTime = performance.now(); - const duration = endTime - startTime; + const endTime = performance.now(); + const duration = endTime - startTime; - console.log(` + console.log(` Strategy: ${strategy} Total iterations: ${iterations} Crashes: ${crashCount} @@ -220,10 +224,12 @@ if (!isCI) { Average time per test: ${(duration / iterations).toFixed(2)}ms `); - // We expect some errors for invalid input, but no crashes - expect(crashCount).toBe(0); - expect(errorCount).toBeGreaterThan(0); - }); + // We expect some errors for invalid input, but no crashes + expect(crashCount).toBe(0); + expect(errorCount).toBeGreaterThan(0); + }, + 10 * 1000, + ); // Additional test for mixed valid/invalid input test("CSS Parser Mixed Input Fuzzing", async () => { diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 842160c315ebf8..da634ba716242b 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -5,9 +5,19 @@ import { describe, expect, test } from "bun:test"; import "harness"; import path from "path"; -import { attrTest, cssTest, indoc, indoc, minify_test, minifyTest, prefix_test } from "./util"; +import { attrTest, cssTest, indoc, minify_test, minifyTest, prefix_test } from "./util"; describe("css tests", () => { + test("edge case", () => { + minifyTest( + // Problem: the value is being printed as Infinity in our restrict_prec thing but the internal thing actually wants it as 3.40282e38px + `.rounded-full { + border-radius: calc(infinity * 1px); + width: calc(infinity * -1px); +}`, + indoc`.rounded-full{border-radius:1e999px;width:-1e999px}`, + ); + }); describe("border_spacing", () => { minifyTest( ` @@ -3254,4 +3264,272 @@ describe("css tests", () => { }, ); }); + + describe("linear-gradient", () => { + minifyTest(".foo { background: linear-gradient(yellow, blue) }", ".foo{background:linear-gradient(#ff0,#00f)}"); + minifyTest( + ".foo { background: linear-gradient(to bottom, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(180deg, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(0.5turn, yellow, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow 10%, blue 20%) }", + ".foo{background:linear-gradient(#ff0 10%,#00f 20%)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue, yellow); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue 10%, yellow 20%); }", + ".foo{background:linear-gradient(#ff0 80%,#00f 90%)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top, blue 10px, yellow 20px); }", + ".foo{background:linear-gradient(0deg,#00f 10px,#ff0 20px)}", + ); + minifyTest( + ".foo { background: linear-gradient(135deg, yellow, blue); }", + ".foo{background:linear-gradient(135deg,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, blue 20%, #0f0); }", + ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", + ); + minifyTest( + ".foo { background: linear-gradient(to top right, red, white, blue) }", + ".foo{background:linear-gradient(to top right,red,#fff,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, blue calc(10% * 2), #0f0); }", + ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 20%, blue); }", + ".foo{background:linear-gradient(#ff0,20%,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 50%, blue); }", + ".foo{background:linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 20px, blue); }", + ".foo{background:linear-gradient(#ff0,20px,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 50px, blue); }", + ".foo{background:linear-gradient(#ff0,50px,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, 50px, blue); }", + ".foo{background:linear-gradient(#ff0,50px,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, red 30% 40%, blue); }", + ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", + ); + minifyTest( + ".foo { background: linear-gradient(yellow, red 30%, red 40%, blue); }", + ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}", + ); + minifyTest(".foo { background: linear-gradient(0, yellow, blue); }", ".foo{background:linear-gradient(#00f,#ff0)}"); + minifyTest( + ".foo { background: -webkit-linear-gradient(yellow, blue) }", + ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-webkit-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-linear-gradient(top right, red, white, blue) }", + ".foo{background:-webkit-linear-gradient(top right,red,#fff,#00f)}", + ); + minifyTest( + ".foo { background: -moz-linear-gradient(yellow, blue) }", + ".foo{background:-moz-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-moz-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-linear-gradient(top right, red, white, blue) }", + ".foo{background:-moz-linear-gradient(top right,red,#fff,#00f)}", + ); + minifyTest( + ".foo { background: -o-linear-gradient(yellow, blue) }", + ".foo{background:-o-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-linear-gradient(bottom, yellow, blue); }", + ".foo{background:-o-linear-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-linear-gradient(top right, red, white, blue) }", + ".foo{background:-o-linear-gradient(top right,red,#fff,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), to(yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),to(#ff0))}", + ); + minifyTest( + ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), color-stop(50%, red), to(yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}", + ); + minifyTest( + ".foo { background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, blue), color-stop(50%, red), color-stop(100%, yellow)) }", + ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}", + ); + minifyTest( + ".foo { background: repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minifyTest( + ".foo { background: -webkit-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-webkit-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minifyTest( + ".foo { background: -moz-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-moz-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minifyTest( + ".foo { background: -o-repeating-linear-gradient(yellow 10px, blue 50px) }", + ".foo{background:-o-repeating-linear-gradient(#ff0 10px,#00f 50px)}", + ); + minifyTest(".foo { background: radial-gradient(yellow, blue) }", ".foo{background:radial-gradient(#ff0,#00f)}"); + minifyTest( + ".foo { background: radial-gradient(at top left, yellow, blue) }", + ".foo{background:radial-gradient(at 0 0,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(5em circle at top left, yellow, blue) }", + ".foo{background:radial-gradient(5em at 0 0,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(circle at 100%, #333, #333 50%, #eee 75%, #333 75%) }", + ".foo{background:radial-gradient(circle at 100%,#333,#333 50%,#eee 75%,#333 75%)}", + ); + minifyTest( + ".foo { background: radial-gradient(farthest-corner circle at 100% 50%, #333, #333 50%, #eee 75%, #333 75%) }", + ".foo{background:radial-gradient(circle at 100%,#333,#333 50%,#eee 75%,#333 75%)}", + ); + minifyTest( + ".foo { background: radial-gradient(farthest-corner circle at 50% 50%, #333, #333 50%, #eee 75%, #333 75%) }", + ".foo{background:radial-gradient(circle,#333,#333 50%,#eee 75%,#333 75%)}", + ); + minifyTest( + ".foo { background: radial-gradient(ellipse at top, #e66465, transparent) }", + ".foo{background:radial-gradient(at top,#e66465,#0000)}", + ); + minifyTest( + ".foo { background: radial-gradient(20px, yellow, blue) }", + ".foo{background:radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(20px 40px, yellow, blue) }", + ".foo{background:radial-gradient(20px 40px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(ellipse 20px 40px, yellow, blue) }", + ".foo{background:radial-gradient(20px 40px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(ellipse calc(20px + 10px) 40px, yellow, blue) }", + ".foo{background:radial-gradient(30px 40px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(circle farthest-side, yellow, blue) }", + ".foo{background:radial-gradient(circle farthest-side,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(farthest-side circle, yellow, blue) }", + ".foo{background:radial-gradient(circle farthest-side,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(ellipse farthest-side, yellow, blue) }", + ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: radial-gradient(farthest-side ellipse, yellow, blue) }", + ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-radial-gradient(yellow, blue) }", + ".foo{background:-webkit-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-radial-gradient(yellow, blue) }", + ".foo{background:-moz-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-radial-gradient(yellow, blue) }", + ".foo{background:-o-radial-gradient(#ff0,#00f)}", + ); + minifyTest( + ".foo { background: repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-webkit-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -moz-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-moz-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -o-repeating-radial-gradient(circle 20px, yellow, blue) }", + ".foo{background:-o-repeating-radial-gradient(20px,#ff0,#00f)}", + ); + minifyTest( + ".foo { background: -webkit-gradient(radial, center center, 0, center center, 100, from(blue), to(yellow)) }", + ".foo{background:-webkit-gradient(radial,50% 50%,0,50% 50%,100,from(#00f),to(#ff0))}", + ); + minifyTest(".foo { background: conic-gradient(#f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); + minifyTest( + ".foo { background: conic-gradient(at 50% 50%, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest( + ".foo { background: conic-gradient(from 0deg, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest(".foo { background: conic-gradient(from 0, #f06, gold) }", ".foo{background:conic-gradient(#f06,gold)}"); + minifyTest( + ".foo { background: conic-gradient(from 0deg at center, #f06, gold) }", + ".foo{background:conic-gradient(#f06,gold)}", + ); + minifyTest( + ".foo { background: conic-gradient(white -50%, black 150%) }", + ".foo{background:conic-gradient(#fff -50%,#000 150%)}", + ); + minifyTest( + ".foo { background: conic-gradient(white -180deg, black 540deg) }", + ".foo{background:conic-gradient(#fff -180deg,#000 540deg)}", + ); + minifyTest( + ".foo { background: conic-gradient(from 45deg, white, black, white) }", + ".foo{background:conic-gradient(from 45deg,#fff,#000,#fff)}", + ); + minifyTest( + ".foo { background: repeating-conic-gradient(from 45deg, white, black, white) }", + ".foo{background:repeating-conic-gradient(from 45deg,#fff,#000,#fff)}", + ); + minifyTest( + ".foo { background: repeating-conic-gradient(black 0deg 25%, white 0deg 50%) }", + ".foo{background:repeating-conic-gradient(#000 0deg 25%,#fff 0deg 50%)}", + ); + }); }); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 645d29ac9e0572..08830c4bfc8736 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -2971,3 +2971,12 @@ describe("serialization", () => { expect(JSON.parse(string, receiver)).toEqual(buffer); }); }); + +it("should not trim utf-8 start bytes at end of string", () => { + // always worked + const buf1 = Buffer.from("e136e1", "hex"); + expect(buf1.toString("utf-8")).toEqual("\uFFFD6\uFFFD"); + // bugged + const buf2 = Buffer.from("36e1", "hex"); + expect(buf2.toString("utf-8")).toEqual("6\uFFFD"); +}); diff --git a/test/js/node/bunfig.toml b/test/js/node/bunfig.toml new file mode 100644 index 00000000000000..cac7f387d5cc4a --- /dev/null +++ b/test/js/node/bunfig.toml @@ -0,0 +1 @@ +preload = ["./harness.ts"] diff --git a/test/js/node/harness.ts b/test/js/node/harness.ts index f8f20089a1d2a5..f723a749ac1e9e 100644 --- a/test/js/node/harness.ts +++ b/test/js/node/harness.ts @@ -1,11 +1,14 @@ -import { AnyFunction } from "bun"; -import { hideFromStackTrace } from "harness"; +/** + * @note this file patches `node:test` via the require cache. + */ +import {AnyFunction} from "bun"; +import {hideFromStackTrace} from "harness"; import assertNode from "node:assert"; type DoneCb = (err?: Error) => any; function noop() {} export function createTest(path: string) { - const { expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach, mock } = Bun.jest(path); + const {expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach, mock} = Bun.jest(path); hideFromStackTrace(expect); @@ -201,11 +204,11 @@ export function createTest(path: string) { let completed = 0; const globalTimer = globalTimeout ? (timers.push( - setTimeout(() => { - console.log("Global Timeout"); - done(new Error("Timed out!")); - }, globalTimeout), - ), + setTimeout(() => { + console.log("Global Timeout"); + done(new Error("Timed out!")); + }, globalTimeout), + ), timers[timers.length - 1]) : undefined; function createDoneCb(timeout?: number) { @@ -213,11 +216,11 @@ export function createTest(path: string) { const timer = timeout !== undefined ? (timers.push( - setTimeout(() => { - console.log("Timeout"); - done(new Error("Timed out!")); - }, timeout), - ), + setTimeout(() => { + console.log("Timeout"); + done(new Error("Timed out!")); + }, timeout), + ), timers[timers.length - 1]) : timeout; return (result?: Error) => { @@ -262,3 +265,113 @@ export function createTest(path: string) { declare namespace Bun { function jest(path: string): typeof import("bun:test"); } + +if (Bun.main.includes("node/test/parallel")) { + function createMockNodeTestModule() { + + interface TestError extends Error { + testStack: string[]; + } + type Context = { + filename: string; + testStack: string[]; + failures: Error[]; + successes: number; + addFailure(err: unknown): TestError; + recordSuccess(): void; + } + const contexts: Record = {} + + // @ts-ignore + let activeSuite: Context = undefined; + + function createContext(key: string): Context { + return { + filename: key, // duplicate for ease-of-use + // entered each time describe, it, etc is called + testStack: [], + failures: [], + successes: 0, + addFailure(err: unknown) { + const error: TestError = (err instanceof Error ? err : new Error(err as any)) as any; + error.testStack = this.testStack; + const testMessage = `Test failed: ${this.testStack.join(" > ")}`; + error.message = testMessage + "\n" + error.message; + this.failures.push(error); + console.error(error); + return error; + }, + recordSuccess() { + const fullname = this.testStack.join(" > "); + console.log("✅ Test passed:", fullname); + this.successes++; + } + } + } + + function getContext() { + const key: string = Bun.main;// module.parent?.filename ?? require.main?.filename ?? __filename; + return activeSuite = (contexts[key] ??= createContext(key)); + } + + async function test(label: string | Function, fn?: Function | undefined) { + if (typeof fn !== "function" && typeof label === "function") { + fn = label; + label = fn.name; + } + const ctx = getContext(); + try { + ctx.testStack.push(label as string); + await fn(); + ctx.recordSuccess(); + } catch (err) { + const error = ctx.addFailure(err); + throw error; + } finally { + ctx.testStack.pop(); + } + } + + function describe(labelOrFn: string | Function, maybeFn?: Function) { + const [label, fn] = (typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn]); + if (typeof fn !== "function") throw new TypeError("Second argument to describe() must be a function."); + + getContext().testStack.push(label); + try { + fn(); + } catch (e) { + getContext().addFailure(e); + throw e + } finally { + getContext().testStack.pop(); + } + + const failures = getContext().failures.length; + const successes = getContext().successes; + console.error(`describe("${label}") finished with ${successes} passed and ${failures} failed tests.`); + if (failures > 0) { + throw new Error(`${failures} tests failed.`); + } + + } + + return { + test, + describe, + } + + } + + require.cache["node:test"] ??= { + exports: createMockNodeTestModule(), + loaded: true, + isPreloading: false, + id: "node:test", + parent: require.main, + filename: "node:test", + children: [], + path: "node:test", + paths: [], + require, + }; +} diff --git a/test/js/node/string_decoder/string-decoder.test.js b/test/js/node/string_decoder/string-decoder.test.js index 5e8463955c3758..1b4e6ab787e5b0 100644 --- a/test/js/node/string_decoder/string-decoder.test.js +++ b/test/js/node/string_decoder/string-decoder.test.js @@ -260,3 +260,21 @@ it("decoding latin1, issue #3738", () => { output += decoder.end(); expect(output).toStrictEqual("ÝYÞ"); }); + +it("invalid utf-8 at end of stream can sometimes produce more than one replacement character", () => { + let decoder = new RealStringDecoder("utf-8"); + expect(decoder.write(Buffer.from("36f59c", "hex"))).toEqual("6"); + expect(decoder.end()).toEqual("\uFFFD\uFFFD"); + decoder = new RealStringDecoder("utf-8"); + expect(decoder.write(Buffer.from("36f5", "hex"))).toEqual("6"); + expect(decoder.end(Buffer.from("9c", "hex"))).toEqual("\uFFFD\uFFFD"); +}); + +it("invalid utf-8 at end of stream can sometimes produce more than one replacement character", () => { + let decoder = new RealStringDecoder("utf-8"); + expect(decoder.write(Buffer.from("36f59c", "hex"))).toEqual("6"); + expect(decoder.end()).toEqual("\uFFFD\uFFFD"); + decoder = new RealStringDecoder("utf-8"); + expect(decoder.write(Buffer.from("36f5", "hex"))).toEqual("6"); + expect(decoder.end(Buffer.from("9c", "hex"))).toEqual("\uFFFD\uFFFD"); +}); diff --git a/test/js/node/test/parallel/test-console-group.js b/test/js/node/test/parallel/test-console-group.js new file mode 100644 index 00000000000000..3870619f360e63 --- /dev/null +++ b/test/js/node/test/parallel/test-console-group.js @@ -0,0 +1,241 @@ +'use strict'; +require('../common'); +const { + hijackStdout, + hijackStderr, + restoreStdout, + restoreStderr +} = require('../common/hijackstdio'); + +const assert = require('assert'); +const Console = require('console').Console; + +let c, stdout, stderr; + +function setup(groupIndentation) { + stdout = ''; + hijackStdout(function(data) { + stdout += data; + }); + + stderr = ''; + hijackStderr(function(data) { + stderr += data; + }); + + c = new Console({ stdout: process.stdout, + stderr: process.stderr, + colorMode: false, + groupIndentation: groupIndentation }); +} + +function teardown() { + restoreStdout(); + restoreStderr(); +} + +// Basic group() functionality +{ + setup(); + const expectedOut = 'This is the outer level\n' + + ' Level 2\n' + + ' Level 3\n' + + ' Back to level 2\n' + + 'Back to the outer level\n' + + 'Still at the outer level\n'; + + + const expectedErr = ' More of level 3\n'; + + c.log('This is the outer level'); + c.group(); + c.log('Level 2'); + c.group(); + c.log('Level 3'); + c.warn('More of level 3'); + c.groupEnd(); + c.log('Back to level 2'); + c.groupEnd(); + c.log('Back to the outer level'); + c.groupEnd(); + c.log('Still at the outer level'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Group indentation is tracked per Console instance. +{ + setup(); + const expectedOut = 'No indentation\n' + + 'None here either\n' + + ' Now the first console is indenting\n' + + 'But the second one does not\n'; + const expectedErr = ''; + + const c2 = new Console(process.stdout, process.stderr); + c.log('No indentation'); + c2.log('None here either'); + c.group(); + c.log('Now the first console is indenting'); + c2.log('But the second one does not'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Make sure labels work. +{ + setup(); + const expectedOut = 'This is a label\n' + + ' And this is the data for that label\n'; + const expectedErr = ''; + + c.group('This is a label'); + c.log('And this is the data for that label'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Check that console.groupCollapsed() is an alias of console.group() +{ + setup(); + const expectedOut = 'Label\n' + + ' Level 2\n' + + ' Level 3\n'; + const expectedErr = ''; + + c.groupCollapsed('Label'); + c.log('Level 2'); + c.groupCollapsed(); + c.log('Level 3'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Check that multiline strings and object output are indented properly. +{ + setup(); + const expectedOut = 'not indented\n' + + ' indented\n' + + ' also indented\n' + + ' {\n' + + " also: 'a',\n" + + " multiline: 'object',\n" + + " should: 'be',\n" + + " indented: 'properly',\n" + + " kthx: 'bai'\n" + + ' }\n'; + const expectedErr = ''; + + c.log('not indented'); + c.group(); + c.log('indented\nalso indented'); + c.log({ also: 'a', + multiline: 'object', + should: 'be', + indented: 'properly', + kthx: 'bai' }); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Check that the kGroupIndent symbol property is not enumerable +{ + const keys = Reflect.ownKeys(console) + .filter((val) => Object.prototype.propertyIsEnumerable.call(console, val)) + .map((val) => val.toString()); + assert(!keys.includes('Symbol(groupIndent)'), + 'groupIndent should not be enumerable'); +} + +// Check custom groupIndentation. +{ + setup(3); + const expectedOut = 'Set the groupIndentation parameter to 3\n' + + 'This is the outer level\n' + + ' Level 2\n' + + ' Level 3\n' + + ' Back to level 2\n' + + 'Back to the outer level\n' + + 'Still at the outer level\n'; + + + const expectedErr = ' More of level 3\n'; + + c.log('Set the groupIndentation parameter to 3'); + c.log('This is the outer level'); + c.group(); + c.log('Level 2'); + c.group(); + c.log('Level 3'); + c.warn('More of level 3'); + c.groupEnd(); + c.log('Back to level 2'); + c.groupEnd(); + c.log('Back to the outer level'); + c.groupEnd(); + c.log('Still at the outer level'); + + assert.strictEqual(stdout, expectedOut); + assert.strictEqual(stderr, expectedErr); + teardown(); +} + +// Check the correctness of the groupIndentation parameter. +{ + // TypeError + [null, 'str', [], false, true, {}].forEach((e) => { + assert.throws( + () => { + new Console({ stdout: process.stdout, + stderr: process.stderr, + groupIndentation: e }); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + }); + + // RangeError for integer + [NaN, 1.01].forEach((e) => { + assert.throws( + () => { + new Console({ stdout: process.stdout, + stderr: process.stderr, + groupIndentation: e }); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: /an integer/, + } + ); + }); + + // RangeError + [-1, 1001].forEach((e) => { + assert.throws( + () => { + new Console({ stdout: process.stdout, + stderr: process.stderr, + groupIndentation: e }); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: />= 0 and <= 1000/, + } + ); + }); +} diff --git a/test/js/node/test/parallel/test-console-instance.js b/test/js/node/test/parallel/test-console-instance.js new file mode 100644 index 00000000000000..9821afeabd6ff1 --- /dev/null +++ b/test/js/node/test/parallel/test-console-instance.js @@ -0,0 +1,146 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const Stream = require('stream'); +const requiredConsole = require('console'); +const Console = requiredConsole.Console; + +const out = new Stream(); +const err = new Stream(); + +// Ensure the Console instance doesn't write to the +// process' "stdout" or "stderr" streams. +process.stdout.write = process.stderr.write = common.mustNotCall(); + +// Make sure that the "Console" function exists. +assert.strictEqual(typeof Console, 'function'); + +assert.strictEqual(requiredConsole, global.console); +// Make sure the custom instanceof of Console works +assert.ok(global.console instanceof Console); +assert.ok(!({} instanceof Console)); + +// Make sure that the Console constructor throws +// when not given a writable stream instance. +assert.throws( + () => { new Console(); }, + { + code: 'ERR_CONSOLE_WRITABLE_STREAM', + name: 'TypeError', + message: /stdout/ + } +); + +// Console constructor should throw if stderr exists but is not writable. +assert.throws( + () => { + out.write = () => {}; + err.write = undefined; + new Console(out, err); + }, + { + code: 'ERR_CONSOLE_WRITABLE_STREAM', + name: 'TypeError', + message: /stderr/ + } +); + +out.write = err.write = (d) => {}; + +{ + const c = new Console(out, err); + assert.ok(c instanceof Console); + + out.write = err.write = common.mustCall((d) => { + assert.strictEqual(d, 'test\n'); + }, 2); + + c.log('test'); + c.error('test'); + + out.write = common.mustCall((d) => { + assert.strictEqual(d, '{ foo: 1 }\n'); + }); + + c.dir({ foo: 1 }); + + // Ensure that the console functions are bound to the console instance. + let called = 0; + out.write = common.mustCall((d) => { + called++; + assert.strictEqual(d, `${called} ${called - 1} [ 1, 2, 3 ]\n`); + }, 3); + + [1, 2, 3].forEach(c.log); +} + +// Test calling Console without the `new` keyword. +{ + const withoutNew = Console(out, err); + assert.ok(withoutNew instanceof Console); +} + +// Test extending Console +{ + class MyConsole extends Console { + hello() {} + // See if the methods on Console.prototype are overridable. + log() { return 'overridden'; } + } + const myConsole = new MyConsole(process.stdout); + assert.strictEqual(typeof myConsole.hello, 'function'); + assert.ok(myConsole instanceof Console); + assert.strictEqual(myConsole.log(), 'overridden'); + + const log = myConsole.log; + assert.strictEqual(log(), 'overridden'); +} + +// Instance that does not ignore the stream errors. +{ + const c2 = new Console(out, err, false); + + out.write = () => { throw new Error('out'); }; + err.write = () => { throw new Error('err'); }; + + assert.throws(() => c2.log('foo'), /^Error: out$/); + assert.throws(() => c2.warn('foo'), /^Error: err$/); + assert.throws(() => c2.dir('foo'), /^Error: out$/); +} + +// Console constructor throws if inspectOptions is not an object. +[null, true, false, 'foo', 5, Symbol()].forEach((inspectOptions) => { + assert.throws( + () => { + new Console({ + stdout: out, + stderr: err, + inspectOptions + }); + }, + { + code: 'ERR_INVALID_ARG_TYPE' + } + ); +}); diff --git a/test/js/node/test/parallel/test-console-no-swallow-stack-overflow.js b/test/js/node/test/parallel/test-console-no-swallow-stack-overflow.js new file mode 100644 index 00000000000000..8510f99488a550 --- /dev/null +++ b/test/js/node/test/parallel/test-console-no-swallow-stack-overflow.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Console } = require('console'); +const { Writable } = require('stream'); + +for (const method of ['dir', 'log', 'warn']) { + assert.throws(() => { + const out = new Writable({ + write: common.mustCall(function write(...args) { + // Exceeds call stack. + // call twice to prevent jsc tail call optimization + write(...args); + return write(...args); + }), + }); + const c = new Console(out, out, true); + + c[method]('Hello, world!'); + }, { name: 'RangeError' }); +} diff --git a/test/js/node/test/parallel/test-console-tty-colors.js b/test/js/node/test/parallel/test-console-tty-colors.js new file mode 100644 index 00000000000000..969fb53a239883 --- /dev/null +++ b/test/js/node/test/parallel/test-console-tty-colors.js @@ -0,0 +1,99 @@ +'use strict'; +// ci sets process.env["FORCE_COLOR"], which makes the test fail in both node and bun +delete process.env["FORCE_COLOR"]; + +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); +const { Writable } = require('stream'); +const { Console } = require('console'); + +function check(isTTY, colorMode, expectedColorMode, inspectOptions) { + const items = [ + 1, + { a: 2 }, + [ 'foo' ], + { '\\a': '\\bar' }, + ]; + + let i = 0; + const stream = new Writable({ + write: common.mustCall((chunk, enc, cb) => { + console.log("testing case", isTTY, colorMode, expectedColorMode, inspectOptions); + assert.strictEqual(chunk.trim(), + util.inspect(items[i++], { + colors: expectedColorMode, + ...inspectOptions + })); + cb(); + }, items.length), + decodeStrings: false + }); + stream.isTTY = isTTY; + + // Set ignoreErrors to `false` here so that we see assertion failures + // from the `write()` call happen. + const testConsole = new Console({ + stdout: stream, + ignoreErrors: false, + colorMode, + inspectOptions + }); + for (const item of items) { + testConsole.log(item); + } +} + +check(true, 'auto', true); +check(false, 'auto', false); +check(false, undefined, true, { colors: true, compact: false }); +check(true, 'auto', true, { compact: false }); +check(true, undefined, false, { colors: false }); +check(true, true, true); +check(false, true, true); +check(true, false, false); +check(false, false, false); + +// Check invalid options. +{ + const stream = new Writable({ + write: common.mustNotCall() + }); + + [0, 'true', null, {}, [], () => {}].forEach((colorMode) => { + const received = util.inspect(colorMode); + assert.throws( + () => { + new Console({ + stdout: stream, + ignoreErrors: false, + colorMode: colorMode + }); + }, + { + message: `The argument 'colorMode' must be one of: 'auto', true, false. Received ${received}`, + code: 'ERR_INVALID_ARG_VALUE' + } + ); + }); + + [true, false, 'auto'].forEach((colorMode) => { + assert.throws( + () => { + new Console({ + stdout: stream, + ignoreErrors: false, + colorMode: colorMode, + inspectOptions: { + colors: false + } + }); + }, + { + message: 'Option "options.inspectOptions.color" cannot be used in ' + + 'combination with option "colorMode"', + code: 'ERR_INCOMPATIBLE_OPTION_PAIR' + } + ); + }); +} diff --git a/test/js/node/test/parallel/test-console-with-frozen-intrinsics.js b/test/js/node/test/parallel/test-console-with-frozen-intrinsics.js new file mode 100644 index 00000000000000..1da2a6a5fb9eaa --- /dev/null +++ b/test/js/node/test/parallel/test-console-with-frozen-intrinsics.js @@ -0,0 +1,30 @@ +// flags: --frozen-intrinsics +'use strict'; +require('../common'); +console.clear(); + +const consoleMethods = ['log', 'info', 'warn', 'error', 'debug', 'trace']; + +for (const method of consoleMethods) { + console[method]('foo'); + console[method]('foo', 'bar'); + console[method]('%s %s', 'foo', 'bar', 'hop'); +} + +console.dir({ slashes: '\\\\' }); +console.dirxml({ slashes: '\\\\' }); + +console.time('label'); +console.timeLog('label', 'hi'); +console.timeEnd('label'); + +console.assert(true, 'true'); + +console.count('label'); +console.countReset('label'); + +console.group('label'); +console.groupCollapsed('label'); +console.groupEnd(); + +console.table([{ a: 1, b: 2 }, { a: 'foo', b: 'bar' }]); diff --git a/test/js/node/test/parallel/test-events-customevent.js b/test/js/node/test/parallel/test-events-customevent.js new file mode 100644 index 00000000000000..0cf36aa91cc567 --- /dev/null +++ b/test/js/node/test/parallel/test-events-customevent.js @@ -0,0 +1,323 @@ +// Flags: --expose-internals + +'use strict'; + +const common = require('../common'); +const { ok, strictEqual, deepStrictEqual, throws } = require('node:assert'); +const { inspect } = require('node:util'); + +{ + ok(CustomEvent); + + // Default string + const tag = Object.prototype.toString.call(new CustomEvent('$')); + strictEqual(tag, '[object CustomEvent]'); +} + +{ + // No argument behavior - throw TypeError + throws(() => { + new CustomEvent(); + }, TypeError); + + throws(() => new CustomEvent(Symbol()), TypeError); + + // Too many arguments passed behavior - ignore additional arguments + const ev = new CustomEvent('foo', {}, {}); + strictEqual(ev.type, 'foo'); +} + +{ + const ev = new CustomEvent('$'); + strictEqual(ev.type, '$'); + strictEqual(ev.bubbles, false); + strictEqual(ev.cancelable, false); + strictEqual(ev.detail, null); +} + +{ + // Coercion to string works + strictEqual(new CustomEvent(1).type, '1'); + strictEqual(new CustomEvent(false).type, 'false'); + strictEqual(new CustomEvent({}).type, String({})); +} + +{ + const ev = new CustomEvent('$', { + detail: 56, + sweet: 'x', + cancelable: true, + }); + strictEqual(ev.type, '$'); + strictEqual(ev.bubbles, false); + strictEqual(ev.cancelable, true); + strictEqual(ev.sweet, undefined); + strictEqual(ev.detail, 56); +} + +{ + // Any types of value for `detail` are acceptable. + ['foo', 1, false, [], {}].forEach((i) => { + const ev = new CustomEvent('$', { detail: i }); + strictEqual(ev.detail, i); + }); +} + +{ + // Readonly `detail` behavior + const ev = new CustomEvent('$', { + detail: 56, + }); + strictEqual(ev.detail, 56); + try { + ev.detail = 96; + // eslint-disable-next-line no-unused-vars + } catch (error) { + common.mustCall()(); + } + strictEqual(ev.detail, 56); +} + +{ + const ev = new Event('$', { + detail: 96, + }); + strictEqual(ev.detail, undefined); +} + +// The following tests verify whether CustomEvent works the same as Event +// except carrying custom data. They're based on `parallel/test-eventtarget.js`. + +{ + const ev = new CustomEvent('$'); + strictEqual(ev.type, '$'); + strictEqual(ev.bubbles, false); + strictEqual(ev.cancelable, false); + strictEqual(ev.detail, null); + + strictEqual(ev.defaultPrevented, false); + strictEqual(typeof ev.timeStamp, 'number'); + + // Compatibility properties with the DOM + deepStrictEqual(ev.composedPath(), []); + strictEqual(ev.returnValue, true); + strictEqual(ev.composed, false); + strictEqual(ev.isTrusted, false); + strictEqual(ev.eventPhase, 0); + strictEqual(ev.cancelBubble, false); + + // Not cancelable + ev.preventDefault(); + strictEqual(ev.defaultPrevented, false); +} + +{ + // Invalid options + ['foo', 1, false].forEach((i) => + throws(() => new CustomEvent('foo', i), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + }), + ); +} + +{ + const ev = new CustomEvent('$'); + strictEqual(ev.constructor.name, 'CustomEvent'); + + // CustomEvent Statics + strictEqual(CustomEvent.NONE, 0); + strictEqual(CustomEvent.CAPTURING_PHASE, 1); + strictEqual(CustomEvent.AT_TARGET, 2); + strictEqual(CustomEvent.BUBBLING_PHASE, 3); + strictEqual(new CustomEvent('foo').eventPhase, CustomEvent.NONE); + + // CustomEvent is a function + strictEqual(CustomEvent.length, 1); +} + +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = true; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.stopPropagation(); + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = 'some-truthy-value'; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = true; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.stopPropagation(); + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = 'some-truthy-value'; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new CustomEvent('foo', { cancelable: true }); + strictEqual(ev.type, 'foo'); + strictEqual(ev.cancelable, true); + strictEqual(ev.defaultPrevented, false); + + ev.preventDefault(); + strictEqual(ev.defaultPrevented, true); +} +{ + const ev = new CustomEvent('foo'); + strictEqual(ev.isTrusted, false); +} + +// Works with EventTarget + +{ + const obj = { sweet: 'x', memory: { x: 56, y: 96 } }; + const et = new EventTarget(); + const ev = new CustomEvent('$', { detail: obj }); + const fn = common.mustCall((event) => { + strictEqual(event, ev); + deepStrictEqual(event.detail, obj); + }); + et.addEventListener('$', fn); + et.dispatchEvent(ev); +} + +{ + const eventTarget = new EventTarget(); + const event = new CustomEvent('$'); + eventTarget.dispatchEvent(event); + strictEqual(event.target, eventTarget); +} + +{ + const obj = { sweet: 'x' }; + const eventTarget = new EventTarget(); + + const ev1 = common.mustCall(function(event) { + strictEqual(event.type, 'foo'); + strictEqual(event.detail, obj); + strictEqual(this, eventTarget); + strictEqual(event.eventPhase, 2); + }, 2); + + const ev2 = { + handleEvent: common.mustCall(function(event) { + strictEqual(event.type, 'foo'); + strictEqual(event.detail, obj); + strictEqual(this, ev2); + }), + }; + + eventTarget.addEventListener('foo', ev1); + eventTarget.addEventListener('foo', ev2, { once: true }); + ok(eventTarget.dispatchEvent(new CustomEvent('foo', { detail: obj }))); + eventTarget.dispatchEvent(new CustomEvent('foo', { detail: obj })); + + eventTarget.removeEventListener('foo', ev1); + eventTarget.dispatchEvent(new CustomEvent('foo')); +} + +{ + // Same event dispatched multiple times. + const obj = { sweet: 'x' }; + const event = new CustomEvent('foo', { detail: obj }); + const eventTarget1 = new EventTarget(); + const eventTarget2 = new EventTarget(); + + eventTarget1.addEventListener( + 'foo', + common.mustCall((event) => { + strictEqual(event.eventPhase, CustomEvent.AT_TARGET); + strictEqual(event.target, eventTarget1); + strictEqual(event.detail, obj); + deepStrictEqual(event.composedPath(), [eventTarget1]); + }), + ); + + eventTarget2.addEventListener( + 'foo', + common.mustCall((event) => { + strictEqual(event.eventPhase, CustomEvent.AT_TARGET); + strictEqual(event.target, eventTarget2); + strictEqual(event.detail, obj); + deepStrictEqual(event.composedPath(), [eventTarget2]); + }), + ); + + eventTarget1.dispatchEvent(event); + strictEqual(event.eventPhase, CustomEvent.NONE); + strictEqual(event.target, eventTarget1); + deepStrictEqual(event.composedPath(), []); + + eventTarget2.dispatchEvent(event); + strictEqual(event.eventPhase, CustomEvent.NONE); + strictEqual(event.target, eventTarget2); + deepStrictEqual(event.composedPath(), []); +} + +{ + const obj = { sweet: 'x' }; + const target = new EventTarget(); + const event = new CustomEvent('foo', { detail: obj }); + + strictEqual(event.target, null); + + target.addEventListener( + 'foo', + common.mustCall((event) => { + strictEqual(event.target, target); + strictEqual(event.currentTarget, target); + strictEqual(event.srcElement, target); + strictEqual(event.detail, obj); + }), + ); + target.dispatchEvent(event); +} + +{ + // Event subclassing + const SubEvent = class extends CustomEvent {}; + const ev = new SubEvent('foo', { detail: 56 }); + const eventTarget = new EventTarget(); + const fn = common.mustCall((event) => { + strictEqual(event, ev); + strictEqual(event.detail, 56); + }); + eventTarget.addEventListener('foo', fn, { once: true }); + eventTarget.dispatchEvent(ev); +} + +// Works with inspect + +{ + const ev = new CustomEvent('test'); + // TODO: unskip + // const evConstructorName = inspect(ev, { + // depth: -1, + // }); + // strictEqual(evConstructorName, 'CustomEvent'); + + const inspectResult = inspect(ev, { + depth: 1, + }); + ok(inspectResult.includes('CustomEvent')); +} diff --git a/test/js/node/test/parallel/test-events-listener-count-with-listener.js b/test/js/node/test/parallel/test-events-listener-count-with-listener.js new file mode 100644 index 00000000000000..1696cb1c902cb9 --- /dev/null +++ b/test/js/node/test/parallel/test-events-listener-count-with-listener.js @@ -0,0 +1,65 @@ +'use strict'; + +const common = require('../common'); +const EventEmitter = require('events'); +const assert = require('assert'); + +const EE = new EventEmitter(); +const handler = common.mustCall(undefined, 3); +const anotherHandler = common.mustCall(); + +assert.strictEqual(EE.listenerCount('event'), 0); +assert.strictEqual(EE.listenerCount('event', handler), 0); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.once('event', handler); + +assert.strictEqual(EE.listenerCount('event'), 1); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.removeAllListeners('event'); + +assert.strictEqual(EE.listenerCount('event'), 0); +assert.strictEqual(EE.listenerCount('event', handler), 0); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.on('event', handler); + +assert.strictEqual(EE.listenerCount('event'), 1); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.once('event', anotherHandler); + +assert.strictEqual(EE.listenerCount('event'), 2); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 1); + +assert.strictEqual(EE.listenerCount('another-event'), 0); +assert.strictEqual(EE.listenerCount('another-event', handler), 0); +assert.strictEqual(EE.listenerCount('another-event', anotherHandler), 0); + +EE.once('event', handler); + +assert.strictEqual(EE.listenerCount('event'), 3); +assert.strictEqual(EE.listenerCount('event', handler), 2); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 1); + +EE.emit('event'); + +assert.strictEqual(EE.listenerCount('event'), 1); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.emit('event'); + +assert.strictEqual(EE.listenerCount('event'), 1); +assert.strictEqual(EE.listenerCount('event', handler), 1); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); + +EE.off('event', handler); + +assert.strictEqual(EE.listenerCount('event'), 0); +assert.strictEqual(EE.listenerCount('event', handler), 0); +assert.strictEqual(EE.listenerCount('event', anotherHandler), 0); diff --git a/test/js/node/test/parallel/test-events-on-async-iterator.js b/test/js/node/test/parallel/test-events-on-async-iterator.js new file mode 100644 index 00000000000000..298b1597db6ee0 --- /dev/null +++ b/test/js/node/test/parallel/test-events-on-async-iterator.js @@ -0,0 +1,427 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { on, EventEmitter, listenerCount } = require('events'); + +async function basic() { + const ee = new EventEmitter(); + process.nextTick(() => { + ee.emit('foo', 'bar'); + // 'bar' is a spurious event, we are testing + // that it does not show up in the iterable + ee.emit('bar', 24); + ee.emit('foo', 42); + }); + + const iterable = on(ee, 'foo'); + + const expected = [['bar'], [42]]; + + for await (const event of iterable) { + const current = expected.shift(); + + assert.deepStrictEqual(current, event); + + if (expected.length === 0) { + break; + } + } + assert.strictEqual(ee.listenerCount('foo'), 0); + assert.strictEqual(ee.listenerCount('error'), 0); +} + +async function invalidArgType() { + assert.throws(() => on({}, 'foo'), common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + })); + + const ee = new EventEmitter(); + + [1, 'hi', null, false, () => {}, Symbol(), 1n].map((options) => { + return assert.throws(() => on(ee, 'foo', options), common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + })); + }); +} + +async function error() { + const ee = new EventEmitter(); + const _err = new Error('kaboom'); + process.nextTick(() => { + ee.emit('error', _err); + }); + + const iterable = on(ee, 'foo'); + let looped = false; + let thrown = false; + + try { + // eslint-disable-next-line no-unused-vars + for await (const event of iterable) { + looped = true; + } + } catch (err) { + thrown = true; + assert.strictEqual(err, _err); + } + assert.strictEqual(thrown, true); + assert.strictEqual(looped, false); +} + +async function errorDelayed() { + const ee = new EventEmitter(); + const _err = new Error('kaboom'); + process.nextTick(() => { + ee.emit('foo', 42); + ee.emit('error', _err); + }); + + const iterable = on(ee, 'foo'); + const expected = [[42]]; + let thrown = false; + + try { + for await (const event of iterable) { + const current = expected.shift(); + assert.deepStrictEqual(current, event); + } + } catch (err) { + thrown = true; + assert.strictEqual(err, _err); + } + assert.strictEqual(thrown, true); + assert.strictEqual(ee.listenerCount('foo'), 0); + assert.strictEqual(ee.listenerCount('error'), 0); +} + +async function throwInLoop() { + const ee = new EventEmitter(); + const _err = new Error('kaboom'); + + process.nextTick(() => { + ee.emit('foo', 42); + }); + + try { + for await (const event of on(ee, 'foo')) { + assert.deepStrictEqual(event, [42]); + throw _err; + } + } catch (err) { + assert.strictEqual(err, _err); + } + + assert.strictEqual(ee.listenerCount('foo'), 0); + assert.strictEqual(ee.listenerCount('error'), 0); +} + +async function next() { + const ee = new EventEmitter(); + const iterable = on(ee, 'foo'); + + process.nextTick(function() { + ee.emit('foo', 'bar'); + ee.emit('foo', 42); + iterable.return(); + }); + + const results = await Promise.all([ + iterable.next(), + iterable.next(), + iterable.next(), + ]); + + assert.deepStrictEqual(results, [{ + value: ['bar'], + done: false, + }, { + value: [42], + done: false, + }, { + value: undefined, + done: true, + }]); + + assert.deepStrictEqual(await iterable.next(), { + value: undefined, + done: true, + }); +} + +async function nextError() { + const ee = new EventEmitter(); + const iterable = on(ee, 'foo'); + const _err = new Error('kaboom'); + process.nextTick(function() { + ee.emit('error', _err); + }); + const results = await Promise.allSettled([ + iterable.next(), + iterable.next(), + iterable.next(), + ]); + assert.deepStrictEqual(results, [{ + status: 'rejected', + reason: _err, + }, { + status: 'fulfilled', + value: { + value: undefined, + done: true, + }, + }, { + status: 'fulfilled', + value: { + value: undefined, + done: true, + }, + }]); + assert.strictEqual(ee.listeners('error').length, 0); +} + +async function iterableThrow() { + const ee = new EventEmitter(); + const iterable = on(ee, 'foo'); + + process.nextTick(() => { + ee.emit('foo', 'bar'); + ee.emit('foo', 42); // lost in the queue + iterable.throw(_err); + }); + + const _err = new Error('kaboom'); + let thrown = false; + + assert.throws(() => { + // No argument + iterable.throw(); + }, { + name: 'TypeError', + }); + + const expected = [['bar'], [42]]; + + try { + for await (const event of iterable) { + assert.deepStrictEqual(event, expected.shift()); + } + } catch (err) { + thrown = true; + assert.strictEqual(err, _err); + } + assert.strictEqual(thrown, true); + assert.strictEqual(expected.length, 0); + assert.strictEqual(ee.listenerCount('foo'), 0); + assert.strictEqual(ee.listenerCount('error'), 0); +} + +async function eventTarget() { + const et = new EventTarget(); + const tick = () => et.dispatchEvent(new Event('tick')); + const interval = setInterval(tick, 0); + let count = 0; + for await (const [ event ] of on(et, 'tick')) { + count++; + assert.strictEqual(event.type, 'tick'); + if (count >= 5) { + break; + } + } + assert.strictEqual(count, 5); + clearInterval(interval); +} + +async function errorListenerCount() { + const et = new EventEmitter(); + on(et, 'foo'); + assert.strictEqual(et.listenerCount('error'), 1); +} + +// async function nodeEventTarget() { +// const et = new NodeEventTarget(); +// const tick = () => et.dispatchEvent(new Event('tick')); +// const interval = setInterval(tick, 0); +// let count = 0; +// for await (const [ event] of on(et, 'tick')) { +// count++; +// assert.strictEqual(event.type, 'tick'); +// if (count >= 5) { +// break; +// } +// } +// assert.strictEqual(count, 5); +// clearInterval(interval); +// } + +async function abortableOnBefore() { + const ee = new EventEmitter(); + const abortedSignal = AbortSignal.abort(); + [1, {}, null, false, 'hi'].forEach((signal) => { + assert.throws(() => on(ee, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + assert.throws(() => on(ee, 'foo', { signal: abortedSignal }), { + name: 'AbortError', + }); +} + +async function eventTargetAbortableOnBefore() { + const et = new EventTarget(); + const abortedSignal = AbortSignal.abort(); + [1, {}, null, false, 'hi'].forEach((signal) => { + assert.throws(() => on(et, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + assert.throws(() => on(et, 'foo', { signal: abortedSignal }), { + name: 'AbortError', + }); +} + +async function abortableOnAfter() { + const ee = new EventEmitter(); + const ac = new AbortController(); + + const i = setInterval(() => ee.emit('foo', 'foo'), 10); + + async function foo() { + for await (const f of on(ee, 'foo', { signal: ac.signal })) { + assert.strictEqual(f, 'foo'); + } + } + + foo().catch(common.mustCall((error) => { + assert.strictEqual(error.name, 'AbortError'); + })).finally(() => { + clearInterval(i); + }); + + process.nextTick(() => ac.abort()); +} + +async function eventTargetAbortableOnAfter() { + const et = new EventTarget(); + const ac = new AbortController(); + + const i = setInterval(() => et.dispatchEvent(new Event('foo')), 10); + + async function foo() { + for await (const f of on(et, 'foo', { signal: ac.signal })) { + assert(f); + } + } + + foo().catch(common.mustCall((error) => { + assert.strictEqual(error.name, 'AbortError'); + })).finally(() => { + clearInterval(i); + }); + + process.nextTick(() => ac.abort()); +} + +async function eventTargetAbortableOnAfter2() { + const et = new EventTarget(); + const ac = new AbortController(); + + const i = setInterval(() => et.dispatchEvent(new Event('foo')), 10); + + async function foo() { + for await (const f of on(et, 'foo', { signal: ac.signal })) { + assert(f); + // Cancel after a single event has been triggered. + ac.abort(); + } + } + + foo().catch(common.mustCall((error) => { + assert.strictEqual(error.name, 'AbortError'); + })).finally(() => { + clearInterval(i); + }); +} + +async function abortableOnAfterDone() { + const ee = new EventEmitter(); + const ac = new AbortController(); + + const i = setInterval(() => ee.emit('foo', 'foo'), 1); + let count = 0; + + async function foo() { + for await (const f of on(ee, 'foo', { signal: ac.signal })) { + assert.strictEqual(f[0], 'foo'); + if (++count === 5) + break; + } + ac.abort(); // No error will occur + } + + foo().finally(() => { + clearInterval(i); + }); +} + +async function abortListenerRemovedAfterComplete() { + const ee = new EventEmitter(); + const ac = new AbortController(); + + const i = setInterval(() => ee.emit('foo', 'foo'), 1); + try { + // Below: either the kEvents map is empty or the 'abort' listener list is empty + + // Return case + const endedIterator = on(ee, 'foo', { signal: ac.signal }); + assert.ok(listenerCount(ac.signal, 'abort') > 0); + endedIterator.return(); + assert.strictEqual(listenerCount(ac.signal, 'abort') ?? listenerCount(ac.signal), 0); + + // Throw case + const throwIterator = on(ee, 'foo', { signal: ac.signal }); + assert.ok(listenerCount(ac.signal, 'abort') > 0); + throwIterator.throw(new Error()); + assert.strictEqual(listenerCount(ac.signal, 'abort') ?? listenerCount(ac.signal), 0); + + // Abort case + on(ee, 'foo', { signal: ac.signal }); + assert.ok(listenerCount(ac.signal, 'abort') > 0); + ac.abort(new Error()); + assert.strictEqual(listenerCount(ac.signal, 'abort') ?? listenerCount(ac.signal), 0); + } finally { + clearInterval(i); + } +} + +async function run() { + const funcs = [ + basic, + invalidArgType, + error, + errorDelayed, + throwInLoop, + next, + nextError, + iterableThrow, + eventTarget, + errorListenerCount, + // nodeEventTarget, + abortableOnBefore, + abortableOnAfter, + eventTargetAbortableOnBefore, + eventTargetAbortableOnAfter, + eventTargetAbortableOnAfter2, + abortableOnAfterDone, + abortListenerRemovedAfterComplete, + ]; + + for (const fn of funcs) { + await fn(); + } +} + +run().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-events-once.js b/test/js/node/test/parallel/test-events-once.js new file mode 100644 index 00000000000000..25357fde67f8d0 --- /dev/null +++ b/test/js/node/test/parallel/test-events-once.js @@ -0,0 +1,287 @@ +'use strict'; +// Flags: --expose-internals --no-warnings + +const common = require('../common'); +const { once, EventEmitter, listenerCount } = require('events'); +const { + deepStrictEqual, + fail, + rejects, + strictEqual, + throws, +} = require('assert'); + +async function onceAnEvent() { + const ee = new EventEmitter(); + + process.nextTick(() => { + ee.emit('myevent', 42); + }); + + const [value] = await once(ee, 'myevent'); + strictEqual(value, 42); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function onceAnEventWithInvalidOptions() { + const ee = new EventEmitter(); + + await Promise.all([1, 'hi', null, false, () => {}, Symbol(), 1n].map((options) => { + return throws(() => once(ee, 'myevent', options), { + code: 'ERR_INVALID_ARG_TYPE', + }); + })); +} + +async function onceAnEventWithTwoArgs() { + const ee = new EventEmitter(); + + process.nextTick(() => { + ee.emit('myevent', 42, 24); + }); + + const value = await once(ee, 'myevent'); + deepStrictEqual(value, [42, 24]); +} + +async function catchesErrors() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + let err; + process.nextTick(() => { + ee.emit('error', expected); + }); + + try { + await once(ee, 'myevent'); + } catch (_e) { + err = _e; + } + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function catchesErrorsWithAbortSignal() { + const ee = new EventEmitter(); + const ac = new AbortController(); + const signal = ac.signal; + + const expected = new Error('boom'); + let err; + process.nextTick(() => { + ee.emit('error', expected); + }); + + try { + const promise = once(ee, 'myevent', { signal }); + strictEqual(ee.listenerCount('error'), 1); + strictEqual(listenerCount(signal, "abort"), 1); + + await promise; + } catch (e) { + err = e; + } + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); + strictEqual(listenerCount(signal, "abort"), 0); +} + +async function stopListeningAfterCatchingError() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + let err; + process.nextTick(() => { + ee.emit('error', expected); + ee.emit('myevent', 42, 24); + }); + + try { + await once(ee, 'myevent'); + } catch (_e) { + err = _e; + } + process.removeAllListeners('multipleResolves'); + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function onceError() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + process.nextTick(() => { + ee.emit('error', expected); + }); + + const promise = once(ee, 'error'); + strictEqual(ee.listenerCount('error'), 1); + const [ err ] = await promise; + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function onceWithEventTarget() { + const et = new EventTarget(); + const event = new Event('myevent'); + process.nextTick(() => { + et.dispatchEvent(event); + }); + const [ value ] = await once(et, 'myevent'); + strictEqual(value, event); +} + +async function onceWithEventTargetError() { + const et = new EventTarget(); + const error = new Event('error'); + process.nextTick(() => { + et.dispatchEvent(error); + }); + + const [ err ] = await once(et, 'error'); + strictEqual(err, error); +} + +async function onceWithInvalidEventEmmiter() { + const ac = new AbortController(); + return throws(() => once(ac, 'myevent'), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +async function prioritizesEventEmitter() { + const ee = new EventEmitter(); + ee.addEventListener = fail; + ee.removeAllListeners = fail; + process.nextTick(() => ee.emit('foo')); + await once(ee, 'foo'); +} + +async function abortSignalBefore() { + const ee = new EventEmitter(); + ee.on('error', common.mustNotCall()); + const abortedSignal = AbortSignal.abort(); + + await Promise.all([1, {}, 'hi', null, false].map((signal) => { + return throws(() => once(ee, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + })); + + return throws(() => once(ee, 'foo', { signal: abortedSignal }), { + name: 'AbortError', + }); +} + +async function abortSignalAfter() { + const ee = new EventEmitter(); + const ac = new AbortController(); + ee.on('error', common.mustNotCall()); + const r = rejects(once(ee, 'foo', { signal: ac.signal }), { + name: 'AbortError', + }); + process.nextTick(() => ac.abort()); + return r; +} + +async function abortSignalAfterEvent() { + const ee = new EventEmitter(); + const ac = new AbortController(); + process.nextTick(() => { + ee.emit('foo'); + ac.abort(); + }); + const promise = once(ee, 'foo', { signal: ac.signal }); + strictEqual(listenerCount(ac.signal, "abort"), 1); + await promise; + strictEqual(listenerCount(ac.signal, "abort"), 0); +} + +async function abortSignalRemoveListener() { + const ee = new EventEmitter(); + const ac = new AbortController(); + + try { + process.nextTick(() => ac.abort()); + await once(ee, 'test', { signal: ac.signal }); + } catch { + strictEqual(ee.listeners('test').length, 0); + strictEqual(ee.listeners('error').length, 0); + } +} + +async function eventTargetAbortSignalBefore() { + const et = new EventTarget(); + const abortedSignal = AbortSignal.abort(); + + await Promise.all([1, {}, 'hi', null, false].map((signal) => { + return throws(() => once(et, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + })); + + return throws(() => once(et, 'foo', { signal: abortedSignal }), { + name: 'AbortError', + }); +} + +// TODO: unskip +// async function eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped() { +// const et = new EventTarget(); +// const ac = new AbortController(); +// const { signal } = ac; +// signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true }); + +// process.nextTick(() => ac.abort()); +// return rejects(once(et, 'foo', { signal }), { +// name: 'AbortError', +// }); +// } + +async function eventTargetAbortSignalAfter() { + const et = new EventTarget(); + const ac = new AbortController(); + const r = rejects(once(et, 'foo', { signal: ac.signal }), { + name: 'AbortError', + }); + process.nextTick(() => ac.abort()); + return r; +} + +async function eventTargetAbortSignalAfterEvent() { + const et = new EventTarget(); + const ac = new AbortController(); + process.nextTick(() => { + et.dispatchEvent(new Event('foo')); + ac.abort(); + }); + await once(et, 'foo', { signal: ac.signal }); +} + +Promise.all([ + onceAnEvent(), + onceAnEventWithInvalidOptions(), + onceAnEventWithTwoArgs(), + catchesErrors(), + catchesErrorsWithAbortSignal(), + stopListeningAfterCatchingError(), + onceError(), + onceWithEventTarget(), + onceWithEventTargetError(), + onceWithInvalidEventEmmiter(), + prioritizesEventEmitter(), + abortSignalBefore(), + abortSignalAfter(), + abortSignalAfterEvent(), + abortSignalRemoveListener(), + eventTargetAbortSignalBefore(), + // eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped(), + eventTargetAbortSignalAfter(), + eventTargetAbortSignalAfterEvent(), +]).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-events-static-geteventlisteners.js b/test/js/node/test/parallel/test-events-static-geteventlisteners.js new file mode 100644 index 00000000000000..35b4e34325a8b5 --- /dev/null +++ b/test/js/node/test/parallel/test-events-static-geteventlisteners.js @@ -0,0 +1,51 @@ +'use strict'; +// Flags: --expose-internals --no-warnings +const common = require('../common'); + +const { + deepStrictEqual, + throws, +} = require('assert'); + +const { getEventListeners, EventEmitter } = require('events'); + +// Test getEventListeners on EventEmitter +{ + const fn1 = common.mustNotCall(); + const fn2 = common.mustNotCall(); + const emitter = new EventEmitter(); + emitter.on('foo', fn1); + emitter.on('foo', fn2); + emitter.on('baz', fn1); + emitter.on('baz', fn1); + deepStrictEqual(getEventListeners(emitter, 'foo'), [fn1, fn2]); + deepStrictEqual(getEventListeners(emitter, 'bar'), []); + deepStrictEqual(getEventListeners(emitter, 'baz'), [fn1, fn1]); +} +// Test getEventListeners on EventTarget +{ + const fn1 = common.mustNotCall(); + const fn2 = common.mustNotCall(); + const target = new EventTarget(); + target.addEventListener('foo', fn1); + target.addEventListener('foo', fn2); + target.addEventListener('baz', fn1); + target.addEventListener('baz', fn1); + deepStrictEqual(getEventListeners(target, 'foo'), [fn1, fn2]); + deepStrictEqual(getEventListeners(target, 'bar'), []); + deepStrictEqual(getEventListeners(target, 'baz'), [fn1]); +} + +{ + throws(() => { + getEventListeners('INVALID_EMITTER'); + }, /ERR_INVALID_ARG_TYPE/); +} +// { +// // Test weak listeners +// const target = new EventTarget(); +// const fn = common.mustNotCall(); +// target.addEventListener('foo', fn, { [kWeakHandler]: {} }); +// const listeners = getEventListeners(target, 'foo'); +// deepStrictEqual(listeners, [fn]); +// } diff --git a/test/js/node/test/parallel/test-path-resolve.js b/test/js/node/test/parallel/test-path-resolve.js new file mode 100644 index 00000000000000..f34eadcc0ccefd --- /dev/null +++ b/test/js/node/test/parallel/test-path-resolve.js @@ -0,0 +1,111 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const child = require('child_process'); +const path = require('path'); + +const failures = []; +const slashRE = /\//g; +const backslashRE = /\\/g; + +const posixyCwd = common.isWindows ? + (() => { + const _ = process.cwd() + .replaceAll(path.sep, path.posix.sep); + return _.slice(_.indexOf(path.posix.sep)); + })() : + process.cwd(); + +const resolveTests = [ + [ path.win32.resolve, + // Arguments result + [[['c:/blah\\blah', 'd:/games', 'c:../a'], 'c:\\blah\\a'], + [['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe'], 'd:\\e.exe'], + [['c:/ignore', 'c:/some/file'], 'c:\\some\\file'], + [['d:/ignore', 'd:some/dir//'], 'd:\\ignore\\some\\dir'], + [['.'], process.cwd()], + [['//server/share', '..', 'relative\\'], '\\\\server\\share\\relative'], + [['c:/', '//'], 'c:\\'], + [['c:/', '//dir'], 'c:\\dir'], + [['c:/', '//server/share'], '\\\\server\\share\\'], + [['c:/', '//server//share'], '\\\\server\\share\\'], + [['c:/', '///some//dir'], 'c:\\some\\dir'], + [['C:\\foo\\tmp.3\\', '..\\tmp.3\\cycles\\root.js'], + 'C:\\foo\\tmp.3\\cycles\\root.js'], + // IMPORTANT NOTE: + // - PR originally landed in #54224 and #55623 to fix issue #54025 + // - It caused a regression (issue #56002) and was reverted in #56088 + // - This behavior did _not_ land in even-numbered versions + // If node decides to adopt this, we need to revisit these tests + // + // [['\\\\.\\PHYSICALDRIVE0'], '\\\\.\\PHYSICALDRIVE0'], + // [['\\\\?\\PHYSICALDRIVE0'], '\\\\?\\PHYSICALDRIVE0'], + [['\\\\.\\PHYSICALDRIVE0'], '\\\\.\\PHYSICALDRIVE0\\'], + [['\\\\?\\PHYSICALDRIVE0'], '\\\\?\\PHYSICALDRIVE0\\'], + ], + ], + [ path.posix.resolve, + // Arguments result + [[['/var/lib', '../', 'file/'], '/var/file'], + [['/var/lib', '/../', 'file/'], '/file'], + [['a/b/c/', '../../..'], posixyCwd], + [['.'], posixyCwd], + [['/some/dir', '.', '/absolute/'], '/absolute'], + [['/foo/tmp.3/', '../tmp.3/cycles/root.js'], '/foo/tmp.3/cycles/root.js'], + ], + ], +]; +resolveTests.forEach(([resolve, tests]) => { + tests.forEach(([test, expected]) => { + const actual = resolve.apply(null, test); + let actualAlt; + const os = resolve === path.win32.resolve ? 'win32' : 'posix'; + if (resolve === path.win32.resolve && !common.isWindows) + actualAlt = actual.replace(backslashRE, '/'); + else if (resolve !== path.win32.resolve && common.isWindows) + actualAlt = actual.replace(slashRE, '\\'); + + const message = + `path.${os}.resolve(${test.map(JSON.stringify).join(',')})\n expect=${ + JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + if (actual !== expected && actualAlt !== expected) + failures.push(message); + }); +}); +assert.strictEqual(failures.length, 0, failures.join('\n')); + +if (common.isWindows) { + // Test resolving the current Windows drive letter from a spawned process. + // See https://github.com/nodejs/node/issues/7215 + const currentDriveLetter = path.parse(process.cwd()).root.substring(0, 2); + const resolveFixture = fixtures.path('path-resolve.js'); + const spawnResult = child.spawnSync( + process.argv[0], [resolveFixture, currentDriveLetter]); + const resolvedPath = spawnResult.stdout.toString().trim(); + assert.strictEqual(resolvedPath.toLowerCase(), process.cwd().toLowerCase()); +} + + +// Test originally was this: +// +// if (!common.isWindows) { +// // Test handling relative paths to be safe when process.cwd() fails. +// process.cwd = () => ''; +// assert.strictEqual(process.cwd(), ''); +// const resolved = path.resolve(); +// const expected = '.'; +// assert.strictEqual(resolved, expected); +// } +// +// In Bun, process.cwd() doesn't affect the behavior of `path.resolve()` (it uses +// getcwd(2)). This has the following implications: +// 1. overriding process.cwd() has no affect on path.resolve(); +// 2. getcwd isn't affected by $CWD, so it cannot be removed that way; +// 3. The Bun CLI caches cwd on BunProcess.m_cachedCwd at startup, so deleting +// it after the process starts keeps `process.cwd()` at the original value; +// 4. If the current directory is deleted before starting bun, the CLI catches +// it and exits with an error code. +// +// Because of all this, I cannot reproduce a scenario where cwd is empty, so +// this test is commented out. diff --git a/test/js/node/test/parallel/test-querystring-escape.js b/test/js/node/test/parallel/test-querystring-escape.js new file mode 100644 index 00000000000000..5f3ea3aedc4d05 --- /dev/null +++ b/test/js/node/test/parallel/test-querystring-escape.js @@ -0,0 +1,41 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const qs = require('querystring'); + +assert.strictEqual(qs.escape(5), '5'); +assert.strictEqual(qs.escape('test'), 'test'); +assert.strictEqual(qs.escape({}), '%5Bobject%20Object%5D'); +assert.strictEqual(qs.escape([5, 10]), '5%2C10'); +assert.strictEqual(qs.escape('Ŋōđĕ'), '%C5%8A%C5%8D%C4%91%C4%95'); +assert.strictEqual(qs.escape('testŊōđĕ'), 'test%C5%8A%C5%8D%C4%91%C4%95'); +assert.strictEqual(qs.escape(`${String.fromCharCode(0xD800 + 1)}test`), + '%F0%90%91%B4est'); + +assert.throws( + () => qs.escape(String.fromCharCode(0xD800 + 1)), + { + code: 'ERR_INVALID_URI', + name: 'URIError', + message: 'URI malformed' + } +); + +// Using toString for objects +assert.strictEqual( + qs.escape({ test: 5, toString: () => 'test', valueOf: () => 10 }), + 'test' +); + +// `toString` is not callable, must throw an error. +// Error message will vary between different JavaScript engines, so only check +// that it is a `TypeError`. +assert.throws(() => qs.escape({ toString: 5 }), TypeError); + +// Should use valueOf instead of non-callable toString. +assert.strictEqual(qs.escape({ toString: 5, valueOf: () => 'test' }), 'test'); + +// Error message will vary between different JavaScript engines, so only check +// that it is a `TypeError`. +assert.throws(() => qs.escape(Symbol('test')), TypeError); diff --git a/test/js/node/test/parallel/test-querystring.js b/test/js/node/test/parallel/test-querystring.js new file mode 100644 index 00000000000000..b24ec5b569bd03 --- /dev/null +++ b/test/js/node/test/parallel/test-querystring.js @@ -0,0 +1,480 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const inspect = require('util').inspect; + +// test using assert +const qs = require('querystring'); + +function createWithNoPrototype(properties) { + const noProto = { __proto__: null }; + properties.forEach((property) => { + noProto[property.key] = property.value; + }); + return noProto; +} +// Folding block, commented to pass gjslint +// {{{ +// [ wonkyQS, canonicalQS, obj ] +const qsTestCases = [ + ['__proto__=1', + '__proto__=1', + createWithNoPrototype([{ key: '__proto__', value: '1' }])], + ['__defineGetter__=asdf', + '__defineGetter__=asdf', + JSON.parse('{"__defineGetter__":"asdf"}')], + ['foo=918854443121279438895193', + 'foo=918854443121279438895193', + { 'foo': '918854443121279438895193' }], + ['foo=bar', 'foo=bar', { 'foo': 'bar' }], + ['foo=bar&foo=quux', 'foo=bar&foo=quux', { 'foo': ['bar', 'quux'] }], + ['foo=1&bar=2', 'foo=1&bar=2', { 'foo': '1', 'bar': '2' }], + ['my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F', + 'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F', + { 'my weird field': 'q1!2"\'w$5&7/z8)?' }], + ['foo%3Dbaz=bar', 'foo%3Dbaz=bar', { 'foo=baz': 'bar' }], + ['foo=baz=bar', 'foo=baz%3Dbar', { 'foo': 'baz=bar' }], + ['str=foo&arr=1&arr=2&arr=3&somenull=&undef=', + 'str=foo&arr=1&arr=2&arr=3&somenull=&undef=', + { 'str': 'foo', + 'arr': ['1', '2', '3'], + 'somenull': '', + 'undef': '' }], + [' foo = bar ', '%20foo%20=%20bar%20', { ' foo ': ' bar ' }], + ['foo=%zx', 'foo=%25zx', { 'foo': '%zx' }], + ['foo=%EF%BF%BD', 'foo=%EF%BF%BD', { 'foo': '\ufffd' }], + // See: https://github.com/joyent/node/issues/1707 + ['hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz', + 'hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz', + { hasOwnProperty: 'x', + toString: 'foo', + valueOf: 'bar', + __defineGetter__: 'baz' }], + // See: https://github.com/joyent/node/issues/3058 + ['foo&bar=baz', 'foo=&bar=baz', { foo: '', bar: 'baz' }], + ['a=b&c&d=e', 'a=b&c=&d=e', { a: 'b', c: '', d: 'e' }], + ['a=b&c=&d=e', 'a=b&c=&d=e', { a: 'b', c: '', d: 'e' }], + ['a=b&=c&d=e', 'a=b&=c&d=e', { 'a': 'b', '': 'c', 'd': 'e' }], + ['a=b&=&c=d', 'a=b&=&c=d', { 'a': 'b', '': '', 'c': 'd' }], + ['&&foo=bar&&', 'foo=bar', { foo: 'bar' }], + ['&', '', {}], + ['&&&&', '', {}], + ['&=&', '=', { '': '' }], + ['&=&=', '=&=', { '': [ '', '' ] }], + ['=', '=', { '': '' }], + ['+', '%20=', { ' ': '' }], + ['+=', '%20=', { ' ': '' }], + ['+&', '%20=', { ' ': '' }], + ['=+', '=%20', { '': ' ' }], + ['+=&', '%20=', { ' ': '' }], + ['a&&b', 'a=&b=', { 'a': '', 'b': '' }], + ['a=a&&b=b', 'a=a&b=b', { 'a': 'a', 'b': 'b' }], + ['&a', 'a=', { 'a': '' }], + ['&=', '=', { '': '' }], + ['a&a&', 'a=&a=', { a: [ '', '' ] }], + ['a&a&a&', 'a=&a=&a=', { a: [ '', '', '' ] }], + ['a&a&a&a&', 'a=&a=&a=&a=', { a: [ '', '', '', '' ] }], + ['a=&a=value&a=', 'a=&a=value&a=', { a: [ '', 'value', '' ] }], + ['foo+bar=baz+quux', 'foo%20bar=baz%20quux', { 'foo bar': 'baz quux' }], + ['+foo=+bar', '%20foo=%20bar', { ' foo': ' bar' }], + ['a+', 'a%20=', { 'a ': '' }], + ['=a+', '=a%20', { '': 'a ' }], + ['a+&', 'a%20=', { 'a ': '' }], + ['=a+&', '=a%20', { '': 'a ' }], + ['%20+', '%20%20=', { ' ': '' }], + ['=%20+', '=%20%20', { '': ' ' }], + ['%20+&', '%20%20=', { ' ': '' }], + ['=%20+&', '=%20%20', { '': ' ' }], + [null, '', {}], + [undefined, '', {}], +]; + +// [ wonkyQS, canonicalQS, obj ] +const qsColonTestCases = [ + ['foo:bar', 'foo:bar', { 'foo': 'bar' }], + ['foo:bar;foo:quux', 'foo:bar;foo:quux', { 'foo': ['bar', 'quux'] }], + ['foo:1&bar:2;baz:quux', + 'foo:1%26bar%3A2;baz:quux', + { 'foo': '1&bar:2', 'baz': 'quux' }], + ['foo%3Abaz:bar', 'foo%3Abaz:bar', { 'foo:baz': 'bar' }], + ['foo:baz:bar', 'foo:baz%3Abar', { 'foo': 'baz:bar' }], +]; + +// [wonkyObj, qs, canonicalObj] +function extendedFunction() {} +extendedFunction.prototype = { a: 'b' }; +const qsWeirdObjects = [ + // eslint-disable-next-line node-core/no-unescaped-regexp-dot + [{ regexp: /./g }, 'regexp=', { 'regexp': '' }], + // eslint-disable-next-line node-core/no-unescaped-regexp-dot + [{ regexp: new RegExp('.', 'g') }, 'regexp=', { 'regexp': '' }], + [{ fn: () => {} }, 'fn=', { 'fn': '' }], + [{ fn: new Function('') }, 'fn=', { 'fn': '' }], + [{ math: Math }, 'math=', { 'math': '' }], + [{ e: extendedFunction }, 'e=', { 'e': '' }], + [{ d: new Date() }, 'd=', { 'd': '' }], + [{ d: Date }, 'd=', { 'd': '' }], + [ + { f: new Boolean(false), t: new Boolean(true) }, + 'f=&t=', + { 'f': '', 't': '' }, + ], + [{ f: false, t: true }, 'f=false&t=true', { 'f': 'false', 't': 'true' }], + [{ n: null }, 'n=', { 'n': '' }], + [{ nan: NaN }, 'nan=', { 'nan': '' }], + [{ inf: Infinity }, 'inf=', { 'inf': '' }], + [{ a: [], b: [] }, '', {}], + [{ a: 1, b: [] }, 'a=1', { 'a': '1' }], +]; +// }}} + +const vm = require('vm'); +const foreignObject = vm.runInNewContext('({"foo": ["bar", "baz"]})'); + +const qsNoMungeTestCases = [ + ['', {}], + ['foo=bar&foo=baz', { 'foo': ['bar', 'baz'] }], + ['foo=bar&foo=baz', foreignObject], + ['blah=burp', { 'blah': 'burp' }], + ['a=!-._~\'()*', { 'a': '!-._~\'()*' }], + ['a=abcdefghijklmnopqrstuvwxyz', { 'a': 'abcdefghijklmnopqrstuvwxyz' }], + ['a=ABCDEFGHIJKLMNOPQRSTUVWXYZ', { 'a': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' }], + ['a=0123456789', { 'a': '0123456789' }], + ['gragh=1&gragh=3&goo=2', { 'gragh': ['1', '3'], 'goo': '2' }], + ['frappucino=muffin&goat%5B%5D=scone&pond=moose', + { 'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose' }], + ['trololol=yes&lololo=no', { 'trololol': 'yes', 'lololo': 'no' }], +]; + +const qsUnescapeTestCases = [ + ['there is nothing to unescape here', + 'there is nothing to unescape here'], + ['there%20are%20several%20spaces%20that%20need%20to%20be%20unescaped', + 'there are several spaces that need to be unescaped'], + ['there%2Qare%0-fake%escaped values in%%%%this%9Hstring', + 'there%2Qare%0-fake%escaped values in%%%%this%9Hstring'], + ['%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37', + ' !"#$%&\'()*+,-./01234567'], + ['%%2a', '%*'], + ['%2sf%2a', '%2sf*'], + ['%2%2af%2a', '%2*f*'], +]; + +assert.strictEqual(qs.parse('id=918854443121279438895193').id, + '918854443121279438895193'); + +function check(actual, expected, input) { + assert(!(actual instanceof Object)); + const actualKeys = Object.keys(actual).sort(); + const expectedKeys = Object.keys(expected).sort(); + let msg; + if (typeof input === 'string') { + msg = `Input: ${inspect(input)}\n` + + `Actual keys: ${inspect(actualKeys)}\n` + + `Expected keys: ${inspect(expectedKeys)}`; + } + assert.deepStrictEqual(actualKeys, expectedKeys, msg); + expectedKeys.forEach((key) => { + if (typeof input === 'string') { + msg = `Input: ${inspect(input)}\n` + + `Key: ${inspect(key)}\n` + + `Actual value: ${inspect(actual[key])}\n` + + `Expected value: ${inspect(expected[key])}`; + } else { + msg = undefined; + } + assert.deepStrictEqual(actual[key], expected[key], msg); + }); +} + +// Test that the canonical qs is parsed properly. +qsTestCases.forEach((testCase) => { + check(qs.parse(testCase[0]), testCase[2], testCase[0]); +}); + +// Test that the colon test cases can do the same +qsColonTestCases.forEach((testCase) => { + check(qs.parse(testCase[0], ';', ':'), testCase[2], testCase[0]); +}); + +// Test the weird objects, that they get parsed properly +qsWeirdObjects.forEach((testCase) => { + check(qs.parse(testCase[1]), testCase[2], testCase[1]); +}); + +qsNoMungeTestCases.forEach((testCase) => { + assert.deepStrictEqual(qs.stringify(testCase[1], '&', '='), testCase[0]); +}); + +// Test the nested qs-in-qs case +{ + const f = qs.parse('a=b&q=x%3Dy%26y%3Dz'); + check(f, createWithNoPrototype([ + { key: 'a', value: 'b' }, + { key: 'q', value: 'x=y&y=z' }, + ])); + + f.q = qs.parse(f.q); + const expectedInternal = createWithNoPrototype([ + { key: 'x', value: 'y' }, + { key: 'y', value: 'z' }, + ]); + check(f.q, expectedInternal); +} + +// nested in colon +{ + const f = qs.parse('a:b;q:x%3Ay%3By%3Az', ';', ':'); + check(f, createWithNoPrototype([ + { key: 'a', value: 'b' }, + { key: 'q', value: 'x:y;y:z' }, + ])); + f.q = qs.parse(f.q, ';', ':'); + const expectedInternal = createWithNoPrototype([ + { key: 'x', value: 'y' }, + { key: 'y', value: 'z' }, + ]); + check(f.q, expectedInternal); +} + +// Now test stringifying + +// basic +qsTestCases.forEach((testCase) => { + assert.strictEqual(qs.stringify(testCase[2]), testCase[1]); +}); + +qsColonTestCases.forEach((testCase) => { + assert.strictEqual(qs.stringify(testCase[2], ';', ':'), testCase[1]); +}); + +qsWeirdObjects.forEach((testCase) => { + assert.strictEqual(qs.stringify(testCase[0]), testCase[1]); +}); + +// BigInt values + +assert.strictEqual(qs.stringify({ foo: 2n ** 1023n }), + 'foo=' + 2n ** 1023n); +assert.strictEqual(qs.stringify([0n, 1n, 2n]), + '0=0&1=1&2=2'); + +assert.strictEqual(qs.stringify({ foo: 2n ** 1023n }, + null, + null, + { encodeURIComponent: (c) => c }), + 'foo=' + 2n ** 1023n); +assert.strictEqual(qs.stringify([0n, 1n, 2n], + null, + null, + { encodeURIComponent: (c) => c }), + '0=0&1=1&2=2'); + +// Invalid surrogate pair throws URIError +assert.throws( + () => qs.stringify({ foo: '\udc00' }), + { + code: 'ERR_INVALID_URI', + name: 'URIError', + message: 'URI malformed' + } +); + +// Coerce numbers to string +assert.strictEqual(qs.stringify({ foo: 0 }), 'foo=0'); +assert.strictEqual(qs.stringify({ foo: -0 }), 'foo=0'); +assert.strictEqual(qs.stringify({ foo: 3 }), 'foo=3'); +assert.strictEqual(qs.stringify({ foo: -72.42 }), 'foo=-72.42'); +assert.strictEqual(qs.stringify({ foo: NaN }), 'foo='); +assert.strictEqual(qs.stringify({ foo: 1e21 }), 'foo=1e%2B21'); +assert.strictEqual(qs.stringify({ foo: Infinity }), 'foo='); + +// nested +{ + const f = qs.stringify({ + a: 'b', + q: qs.stringify({ + x: 'y', + y: 'z' + }) + }); + assert.strictEqual(f, 'a=b&q=x%3Dy%26y%3Dz'); +} + +qs.parse(undefined); // Should not throw. + +// nested in colon +{ + const f = qs.stringify({ + a: 'b', + q: qs.stringify({ + x: 'y', + y: 'z' + }, ';', ':') + }, ';', ':'); + assert.strictEqual(f, 'a:b;q:x%3Ay%3By%3Az'); +} + +// empty string +assert.strictEqual(qs.stringify(), ''); +assert.strictEqual(qs.stringify(0), ''); +assert.strictEqual(qs.stringify([]), ''); +assert.strictEqual(qs.stringify(null), ''); +assert.strictEqual(qs.stringify(true), ''); + +check(qs.parse(), {}); + +// empty sep +check(qs.parse('a', []), { a: '' }); + +// empty eq +check(qs.parse('a', null, []), { '': 'a' }); + +// Test limiting +assert.strictEqual( + Object.keys(qs.parse('a=1&b=1&c=1', null, null, { maxKeys: 1 })).length, + 1); + +// Test limiting with a case that starts from `&` +assert.strictEqual( + Object.keys(qs.parse('&a', null, null, { maxKeys: 1 })).length, + 0); + +// Test removing limit +{ + function testUnlimitedKeys() { + const query = {}; + + for (let i = 0; i < 2000; i++) query[i] = i; + + const url = qs.stringify(query); + + assert.strictEqual( + Object.keys(qs.parse(url, null, null, { maxKeys: 0 })).length, + 2000); + } + + testUnlimitedKeys(); +} + +{ + const b = qs.unescapeBuffer('%d3%f2Ug%1f6v%24%5e%98%cb' + + '%0d%ac%a2%2f%9d%eb%d8%a2%e6'); + // + assert.strictEqual(b[0], 0xd3); + assert.strictEqual(b[1], 0xf2); + assert.strictEqual(b[2], 0x55); + assert.strictEqual(b[3], 0x67); + assert.strictEqual(b[4], 0x1f); + assert.strictEqual(b[5], 0x36); + assert.strictEqual(b[6], 0x76); + assert.strictEqual(b[7], 0x24); + assert.strictEqual(b[8], 0x5e); + assert.strictEqual(b[9], 0x98); + assert.strictEqual(b[10], 0xcb); + assert.strictEqual(b[11], 0x0d); + assert.strictEqual(b[12], 0xac); + assert.strictEqual(b[13], 0xa2); + assert.strictEqual(b[14], 0x2f); + assert.strictEqual(b[15], 0x9d); + assert.strictEqual(b[16], 0xeb); + assert.strictEqual(b[17], 0xd8); + assert.strictEqual(b[18], 0xa2); + assert.strictEqual(b[19], 0xe6); +} + +assert.strictEqual(qs.unescapeBuffer('a+b', true).toString(), 'a b'); +assert.strictEqual(qs.unescapeBuffer('a+b').toString(), 'a+b'); +assert.strictEqual(qs.unescapeBuffer('a%').toString(), 'a%'); +assert.strictEqual(qs.unescapeBuffer('a%2').toString(), 'a%2'); +assert.strictEqual(qs.unescapeBuffer('a%20').toString(), 'a '); +assert.strictEqual(qs.unescapeBuffer('a%2g').toString(), 'a%2g'); +assert.strictEqual(qs.unescapeBuffer('a%%').toString(), 'a%%'); + +// Test invalid encoded string +check(qs.parse('%\u0100=%\u0101'), { '%Ā': '%ā' }); + +// Test custom decode +{ + function demoDecode(str) { + return str + str; + } + + check( + qs.parse('a=a&b=b&c=c', null, null, { decodeURIComponent: demoDecode }), + { aa: 'aa', bb: 'bb', cc: 'cc' }); + check( + qs.parse('a=a&b=b&c=c', null, '==', { decodeURIComponent: (str) => str }), + { 'a=a': '', 'b=b': '', 'c=c': '' }); +} + +// Test QueryString.unescape +{ + function errDecode(str) { + throw new Error('To jump to the catch scope'); + } + + check(qs.parse('a=a', null, null, { decodeURIComponent: errDecode }), + { a: 'a' }); +} + +// Test custom encode +{ + function demoEncode(str) { + return str[0]; + } + + const obj = { aa: 'aa', bb: 'bb', cc: 'cc' }; + assert.strictEqual( + qs.stringify(obj, null, null, { encodeURIComponent: demoEncode }), + 'a=a&b=b&c=c'); +} + +// Test custom encode for different types +{ + const obj = { number: 1, bigint: 2n, true: true, false: false, object: {} }; + assert.strictEqual( + qs.stringify(obj, null, null, { encodeURIComponent: (v) => v }), + 'number=1&bigint=2&true=true&false=false&object='); +} + +// Test QueryString.unescapeBuffer +qsUnescapeTestCases.forEach((testCase) => { + assert.strictEqual(qs.unescape(testCase[0]), testCase[1]); + assert.strictEqual(qs.unescapeBuffer(testCase[0]).toString(), testCase[1]); +}); + +// Test overriding .unescape +{ + const prevUnescape = qs.unescape; + qs.unescape = (str) => { + return str.replace(/o/g, '_'); + }; + check( + qs.parse('foo=bor'), + createWithNoPrototype([{ key: 'f__', value: 'b_r' }])); + qs.unescape = prevUnescape; +} +// Test separator and "equals" parsing order +check(qs.parse('foo&bar', '&', '&'), { foo: '', bar: '' }); diff --git a/test/js/node/test/parallel/test-string-decoder-fuzz.js b/test/js/node/test/parallel/test-string-decoder-fuzz.js new file mode 100644 index 00000000000000..542876e96e2c5c --- /dev/null +++ b/test/js/node/test/parallel/test-string-decoder-fuzz.js @@ -0,0 +1,49 @@ +'use strict'; +require('../common'); +const { StringDecoder } = require('string_decoder'); +const util = require('util'); +const assert = require('assert'); + +// Tests that, for random sequences of bytes, our StringDecoder gives the +// same result as a direction conversion using Buffer.toString(). +// In particular, it checks that StringDecoder aligns with V8’s own output. + +function rand(max) { + return Math.floor(Math.random() * max); +} + +function randBuf(maxLen) { + const buf = Buffer.allocUnsafe(rand(maxLen)); + for (let i = 0; i < buf.length; i++) + buf[i] = rand(256); + return buf; +} + +const encodings = [ + 'utf16le', 'utf8', 'ascii', 'hex', 'base64', 'latin1', 'base64url', +]; + +function runSingleFuzzTest() { + const enc = encodings[rand(encodings.length)]; + const sd = new StringDecoder(enc); + const bufs = []; + const strings = []; + + const N = rand(10); + for (let i = 0; i < N; ++i) { + const buf = randBuf(50); + bufs.push(buf); + strings.push(sd.write(buf)); + } + strings.push(sd.end()); + + assert.strictEqual(strings.join(''), Buffer.concat(bufs).toString(enc), + `Mismatch:\n${util.inspect(strings)}\n` + + util.inspect(bufs.map((buf) => buf.toString('hex'))) + + `\nfor encoding ${enc}`); +} + +const start = Date.now(); +// Run this for 1 second +while (Date.now() - start < 1000) + runSingleFuzzTest(); diff --git a/test/js/node/test/parallel/test-string-decoder.js b/test/js/node/test/parallel/test-string-decoder.js new file mode 100644 index 00000000000000..d82a149bf2d862 --- /dev/null +++ b/test/js/node/test/parallel/test-string-decoder.js @@ -0,0 +1,287 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const inspect = require('util').inspect; +const StringDecoder = require('string_decoder').StringDecoder; + +// Test default encoding +let decoder = new StringDecoder(); +assert.strictEqual(decoder.encoding, 'utf8'); + +// Should work without 'new' keyword +const decoder2 = {}; +StringDecoder.call(decoder2); +assert.strictEqual(decoder2.encoding, 'utf8'); + +// UTF-8 +test('utf-8', Buffer.from('$', 'utf-8'), '$'); +test('utf-8', Buffer.from('¢', 'utf-8'), '¢'); +test('utf-8', Buffer.from('€', 'utf-8'), '€'); +test('utf-8', Buffer.from('𤭢', 'utf-8'), '𤭢'); +// A mixed ascii and non-ascii string +// Test stolen from deps/v8/test/cctest/test-strings.cc +// U+02E4 -> CB A4 +// U+0064 -> 64 +// U+12E4 -> E1 8B A4 +// U+0030 -> 30 +// U+3045 -> E3 81 85 +test( + 'utf-8', + Buffer.from([0xCB, 0xA4, 0x64, 0xE1, 0x8B, 0xA4, 0x30, 0xE3, 0x81, 0x85]), + '\u02e4\u0064\u12e4\u0030\u3045' +); + +// Some invalid input, known to have caused trouble with chunking +// in https://github.com/nodejs/node/pull/7310#issuecomment-226445923 +// 00: |00000000 ASCII +// 41: |01000001 ASCII +// B8: 10|111000 continuation +// CC: 110|01100 two-byte head +// E2: 1110|0010 three-byte head +// F0: 11110|000 four-byte head +// F1: 11110|001'another four-byte head +// FB: 111110|11 "five-byte head", not UTF-8 +test('utf-8', Buffer.from('C9B5A941', 'hex'), '\u0275\ufffdA'); +test('utf-8', Buffer.from('E2', 'hex'), '\ufffd'); +test('utf-8', Buffer.from('E241', 'hex'), '\ufffdA'); +test('utf-8', Buffer.from('CCCCB8', 'hex'), '\ufffd\u0338'); +test('utf-8', Buffer.from('F0B841', 'hex'), '\ufffdA'); +test('utf-8', Buffer.from('F1CCB8', 'hex'), '\ufffd\u0338'); +test('utf-8', Buffer.from('F0FB00', 'hex'), '\ufffd\ufffd\0'); +test('utf-8', Buffer.from('CCE2B8B8', 'hex'), '\ufffd\u2e38'); +test('utf-8', Buffer.from('E2B8CCB8', 'hex'), '\ufffd\u0338'); +test('utf-8', Buffer.from('E2FBCC01', 'hex'), '\ufffd\ufffd\ufffd\u0001'); +test('utf-8', Buffer.from('CCB8CDB9', 'hex'), '\u0338\u0379'); +// CESU-8 of U+1D40D + +// V8 has changed their invalid UTF-8 handling, see +// https://chromium-review.googlesource.com/c/v8/v8/+/671020 for more info. +test('utf-8', Buffer.from('EDA0B5EDB08D', 'hex'), + '\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd'); + +// UCS-2 +test('ucs2', Buffer.from('ababc', 'ucs2'), 'ababc'); + +// UTF-16LE +test('utf16le', Buffer.from('3DD84DDC', 'hex'), '\ud83d\udc4d'); // thumbs up + +// Additional UTF-8 tests +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('E1', 'hex')), ''); + +// A quick test for lastChar, lastNeed & lastTotal which are undocumented. +assert(decoder.lastChar.equals(new Uint8Array([0xe1, 0, 0, 0]))); +assert.strictEqual(decoder.lastNeed, 2); +assert.strictEqual(decoder.lastTotal, 3); + +assert.strictEqual(decoder.end(), '\ufffd'); + +// ArrayBufferView tests +const arrayBufferViewStr = 'String for ArrayBufferView tests\n'; +const inputBuffer = Buffer.from(arrayBufferViewStr.repeat(8), 'utf8'); +for (const expectView of common.getArrayBufferViews(inputBuffer)) { + assert.strictEqual( + decoder.write(expectView), + inputBuffer.toString('utf8') + ); + assert.strictEqual(decoder.end(), ''); +} + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('E18B', 'hex')), ''); +assert.strictEqual(decoder.end(), '\ufffd'); + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('\ufffd')), '\ufffd'); +assert.strictEqual(decoder.end(), ''); + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('\ufffd\ufffd\ufffd')), + '\ufffd\ufffd\ufffd'); +assert.strictEqual(decoder.end(), ''); + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('EFBFBDE2', 'hex')), '\ufffd'); +assert.strictEqual(decoder.end(), '\ufffd'); + +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('F1', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('41F2', 'hex')), '\ufffdA'); +assert.strictEqual(decoder.end(), '\ufffd'); + +// Additional utf8Text test +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.text(Buffer.from([0x41]), 2), ''); + +// Additional UTF-16LE surrogate pair tests +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.from('3DD8', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('4D', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('DC', 'hex')), '\ud83d\udc4d'); +assert.strictEqual(decoder.end(), ''); + +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.from('3DD8', 'hex')), ''); +assert.strictEqual(decoder.end(), '\ud83d'); + +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.from('3DD8', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('4D', 'hex')), ''); +assert.strictEqual(decoder.end(), '\ud83d'); + +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.from('3DD84D', 'hex')), '\ud83d'); +assert.strictEqual(decoder.end(), ''); + +// Regression test for https://github.com/nodejs/node/issues/22358 +// (unaligned UTF-16 access). +decoder = new StringDecoder('utf16le'); +assert.strictEqual(decoder.write(Buffer.alloc(1)), ''); +assert.strictEqual(decoder.write(Buffer.alloc(20)), '\0'.repeat(10)); +assert.strictEqual(decoder.write(Buffer.alloc(48)), '\0'.repeat(24)); +assert.strictEqual(decoder.end(), ''); + +// Regression tests for https://github.com/nodejs/node/issues/22626 +// (not enough replacement chars when having seen more than one byte of an +// incomplete multibyte characters). +decoder = new StringDecoder('utf8'); +assert.strictEqual(decoder.write(Buffer.from('f69b', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('d1', 'hex')), '\ufffd\ufffd'); +assert.strictEqual(decoder.end(), '\ufffd'); +assert.strictEqual(decoder.write(Buffer.from('f4', 'hex')), ''); +assert.strictEqual(decoder.write(Buffer.from('bde5', 'hex')), '\ufffd\ufffd'); +assert.strictEqual(decoder.end(), '\ufffd'); + +assert.throws( + () => new StringDecoder(1), + { + code: 'ERR_UNKNOWN_ENCODING', + name: 'TypeError', + message: 'Unknown encoding: 1' + } +); + +assert.throws( + () => new StringDecoder('test'), + { + code: 'ERR_UNKNOWN_ENCODING', + name: 'TypeError', + message: 'Unknown encoding: test' + } +); + +assert.throws( + () => new StringDecoder('utf8').write(null), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "buf" argument must be an instance of Buffer, TypedArray,' + + ' or DataView. Received null' + } +); + +// Skipped in Bun: JSC supports much larger strings, so it is extremely hard to +// actually produce this exception. +// if (common.enoughTestMem) { +// assert.throws( +// () => new StringDecoder().write(Buffer.alloc((process.arch === 'ia32' ? 0x18ffffe8 : 0x1fffffe8) + 1).fill('a')), +// { +// code: 'ERR_STRING_TOO_LONG', +// } +// ); +// } + +assert.throws( + () => new StringDecoder('utf8').__proto__.write(Buffer.from('abc')), // eslint-disable-line no-proto + { + code: 'ERR_INVALID_THIS', + } +); + +// Test verifies that StringDecoder will correctly decode the given input +// buffer with the given encoding to the expected output. It will attempt all +// possible ways to write() the input buffer, see writeSequences(). The +// singleSequence allows for easy debugging of a specific sequence which is +// useful in case of test failures. +function test(encoding, input, expected, singleSequence) { + let sequences; + if (!singleSequence) { + sequences = writeSequences(input.length); + } else { + sequences = [singleSequence]; + } + const hexNumberRE = /.{2}/g; + sequences.forEach((sequence) => { + const decoder = new StringDecoder(encoding); + let output = ''; + sequence.forEach((write) => { + output += decoder.write(input.slice(write[0], write[1])); + }); + output += decoder.end(); + if (output !== expected) { + const message = + `Expected "${unicodeEscape(expected)}", ` + + `but got "${unicodeEscape(output)}"\n` + + `input: ${input.toString('hex').match(hexNumberRE)}\n` + + `Write sequence: ${JSON.stringify(sequence)}\n` + + `Full Decoder State: ${inspect(decoder)}`; + assert.fail(message); + } + }); +} + +// unicodeEscape prints the str contents as unicode escape codes. +function unicodeEscape(str) { + let r = ''; + for (let i = 0; i < str.length; i++) { + r += `\\u${str.charCodeAt(i).toString(16)}`; + } + return r; +} + +// writeSequences returns an array of arrays that describes all possible ways a +// buffer of the given length could be split up and passed to sequential write +// calls. +// +// e.G. writeSequences(3) will return: [ +// [ [ 0, 3 ] ], +// [ [ 0, 2 ], [ 2, 3 ] ], +// [ [ 0, 1 ], [ 1, 3 ] ], +// [ [ 0, 1 ], [ 1, 2 ], [ 2, 3 ] ] +// ] +function writeSequences(length, start, sequence) { + if (start === undefined) { + start = 0; + sequence = []; + } else if (start === length) { + return [sequence]; + } + let sequences = []; + for (let end = length; end > start; end--) { + const subSequence = sequence.concat([[start, end]]); + const subSequences = writeSequences(length, end, subSequence, sequences); + sequences = sequences.concat(subSequences); + } + return sequences; +}