diff --git a/Cargo.lock b/Cargo.lock index 377fa066..c2807fde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -917,10 +917,12 @@ dependencies = [ [[package]] name = "libdd-capabilities" version = "2.1.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", "bytes", + "futures-channel", + "futures-util", "http", "thiserror", ] @@ -941,7 +943,7 @@ dependencies = [ [[package]] name = "libdd-capabilities-impl" version = "3.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", "bytes", @@ -990,7 +992,7 @@ dependencies = [ [[package]] name = "libdd-common" version = "5.1.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", "bytes", @@ -1053,7 +1055,7 @@ dependencies = [ [[package]] name = "libdd-data-pipeline" version = "7.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", "arc-swap", @@ -1072,7 +1074,7 @@ dependencies = [ "libdd-shared-runtime 2.0.0", "libdd-tinybytes", "libdd-trace-normalization", - "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f)", + "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f)", "libdd-trace-stats", "libdd-trace-utils", "rmp-serde", @@ -1097,7 +1099,7 @@ dependencies = [ [[package]] name = "libdd-ddsketch" version = "1.1.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "prost", ] @@ -1105,13 +1107,16 @@ dependencies = [ [[package]] name = "libdd-dogstatsd-client" version = "4.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", + "async-trait", "cadence", "http", "libdd-common 5.1.0", + "libdd-shared-runtime 2.0.0", "serde", + "tokio", "tracing", ] @@ -1163,7 +1168,7 @@ dependencies = [ [[package]] name = "libdd-shared-runtime" version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "async-trait", "futures", @@ -1207,7 +1212,7 @@ dependencies = [ [[package]] name = "libdd-tinybytes" version = "1.1.1" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "serde", ] @@ -1215,21 +1220,21 @@ dependencies = [ [[package]] name = "libdd-trace-normalization" version = "3.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", - "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f)", + "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f)", ] [[package]] name = "libdd-trace-obfuscation" version = "5.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", "fluent-uri", "libdd-common 5.1.0", - "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f)", + "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f)", "libdd-trace-utils", "log", "percent-encoding", @@ -1250,7 +1255,7 @@ dependencies = [ [[package]] name = "libdd-trace-protobuf" version = "4.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "prost", "serde", @@ -1260,7 +1265,7 @@ dependencies = [ [[package]] name = "libdd-trace-stats" version = "6.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", "arc-swap", @@ -1275,7 +1280,7 @@ dependencies = [ "libdd-dogstatsd-client", "libdd-shared-runtime 2.0.0", "libdd-trace-obfuscation", - "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f)", + "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f)", "libdd-trace-utils", "rmp-serde", "serde", @@ -1288,7 +1293,7 @@ dependencies = [ [[package]] name = "libdd-trace-utils" version = "9.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f#3081603d3c74f209be4e3be951f78a1a7469397f" +source = "git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f#3cdb794e501bc37e52090b032f0b4be116f9a26f" dependencies = [ "anyhow", "base64", @@ -1306,7 +1311,7 @@ dependencies = [ "libdd-common 5.1.0", "libdd-tinybytes", "libdd-trace-normalization", - "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f)", + "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f)", "prost", "rand", "rmp", @@ -1797,7 +1802,7 @@ dependencies = [ "libdd-common 5.1.0", "libdd-data-pipeline", "libdd-shared-runtime 2.0.0", - "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3081603d3c74f209be4e3be951f78a1a7469397f)", + "libdd-trace-protobuf 4.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=3cdb794e501bc37e52090b032f0b4be116f9a26f)", "libdd-trace-stats", "libdd-trace-utils", "rmp-serde", diff --git a/crates/capabilities/Cargo.toml b/crates/capabilities/Cargo.toml index edb40adc..67395893 100644 --- a/crates/capabilities/Cargo.toml +++ b/crates/capabilities/Cargo.toml @@ -15,7 +15,7 @@ http = "1" bytes = "1.4" futures-core = "0.3" anyhow = "1" -libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", rev = "3081603d3c74f209be4e3be951f78a1a7469397f" } +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", rev = "3cdb794e501bc37e52090b032f0b4be116f9a26f" } [dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/crates/capabilities/src/http.rs b/crates/capabilities/src/http.rs index 4248cb30..2ee1bf78 100644 --- a/crates/capabilities/src/http.rs +++ b/crates/capabilities/src/http.rs @@ -59,6 +59,10 @@ impl HttpClientCapability for WasmHttpClient { Self } + fn new_without_connection_pooling() -> Self { + Self + } + #[allow(clippy::manual_async_fn)] fn request( &self, diff --git a/crates/capabilities/src/lib.rs b/crates/capabilities/src/lib.rs index e76a3812..1dd1e034 100644 --- a/crates/capabilities/src/lib.rs +++ b/crates/capabilities/src/lib.rs @@ -14,16 +14,19 @@ use std::time::Duration; use libdd_capabilities::env::{EnvCapability, EnvError}; use libdd_capabilities::file::{FileCapability, FileError, FileMetadata}; use libdd_capabilities::http::HttpError; +use libdd_capabilities::regex::{Captures, Match, RegexCapability, RegexError}; use libdd_capabilities::{HttpClientCapability, LogWriterCapability, MaybeSend, SleepCapability}; pub mod env; pub mod file; pub mod http; +pub mod regex; pub mod sleep; pub use env::WasmEnvCapability; pub use file::WasmFileCapability; pub use http::WasmHttpClient; +pub use regex::{WasmRegexCapability, WasmRegexHandle}; pub use sleep::WasmSleepCapability; /// Bundle of wasm platform capabilities for libdatadog's `TraceExporter`. @@ -65,6 +68,10 @@ impl HttpClientCapability for WasmCapabilities { Self::new() } + fn new_without_connection_pooling() -> Self { + Self::new() + } + fn request( &self, req: ::http::Request<::bytes::Bytes>, @@ -135,3 +142,35 @@ impl EnvCapability for WasmCapabilities { self.env.get(name) } } + +impl RegexCapability for WasmCapabilities { + type Handle = ::Handle; + + fn compile(pattern: &str) -> Result { + WasmRegexCapability::compile(pattern) + } + + fn is_match(handle: &Self::Handle, haystack: &str) -> bool { + WasmRegexCapability::is_match(handle, haystack) + } + + fn find(handle: &Self::Handle, haystack: &str) -> Option { + WasmRegexCapability::find(handle, haystack) + } + + fn find_all(handle: &Self::Handle, haystack: &str) -> Vec { + WasmRegexCapability::find_all(handle, haystack) + } + + fn captures(handle: &Self::Handle, haystack: &str) -> Option { + WasmRegexCapability::captures(handle, haystack) + } + + fn captures_all(handle: &Self::Handle, haystack: &str) -> Vec { + WasmRegexCapability::captures_all(handle, haystack) + } + + fn pattern(handle: &Self::Handle) -> &str { + WasmRegexCapability::pattern(handle) + } +} diff --git a/crates/capabilities/src/regex.js b/crates/capabilities/src/regex.js new file mode 100644 index 00000000..b1122e53 --- /dev/null +++ b/crates/capabilities/src/regex.js @@ -0,0 +1,144 @@ +// Match offsets returned to Rust are UTF-8 byte offsets; JS `RegExp.exec` +// returns UTF-16 code-unit indices, so we translate here. + +'use strict' + +// UTF-8 encoding thresholds (max code point + 1 for each byte-length class). +const ONE_BYTE_MAX = 0x80 // U+0000..U+007F → 1 byte +const TWO_BYTE_MAX = 0x8_00 // U+0080..U+07FF → 2 bytes +// UTF-16 surrogate ranges. A high+low pair encodes one supplementary code +// point (U+10000..U+10FFFF) into 4 UTF-8 bytes. +const HIGH_SURROGATE_MIN = 0xD8_00 +const HIGH_SURROGATE_MAX = 0xDB_FF +const LOW_SURROGATE_MIN = 0xDC_00 +const LOW_SURROGATE_MAX = 0xDF_FF + +// UTF-8 byte cost of the single code unit at `hay[i]`. +// A surrogate pair encodes as 4 UTF-8 bytes but spans two code-unit +// positions; we split those 4 bytes evenly (2 for the high half, 2 for the +// low half) so mid-pair positions get a distinct, monotonic byte offset. +// Lone surrogates count as 3 bytes (WTF-8 style, matching how Node's UTF-8 +// encoder replaces unpaired surrogates with U+FFFD). +/* eslint-disable unicorn/prefer-code-point */ +function cuBytes (hay, i, len) { + const cu = hay.charCodeAt(i) + if (cu < ONE_BYTE_MAX) return 1 + if (cu < TWO_BYTE_MAX) return 2 + if (cu >= HIGH_SURROGATE_MIN && cu <= HIGH_SURROGATE_MAX) { + if (i + 1 < len) { + const low = hay.charCodeAt(i + 1) + if (low >= LOW_SURROGATE_MIN && low <= LOW_SURROGATE_MAX) return 2 + } + return 3 // lone high surrogate + } + if (cu >= LOW_SURROGATE_MIN && cu <= LOW_SURROGATE_MAX) { + if (i > 0) { + const high = hay.charCodeAt(i - 1) + if (high >= HIGH_SURROGATE_MIN && high <= HIGH_SURROGATE_MAX) return 2 + } + return 3 // lone low surrogate + } + return 3 // BMP ≥ U+0800 +} + +// Advance the walker `state` (`i`: code-unit position, `byte`: UTF-8 byte +// offset at that position) forward one code unit at a time until `i` reaches +// `target`. Stepping CU-by-CU (rather than treating a surrogate pair as one +// atomic step) keeps state consistent when `target` lands mid-pair. +function walkTo (hay, state, target) { + const len = hay.length + let i = state.i + let byte = state.byte + while (i < target) { + byte += cuBytes(hay, i, len) + i += 1 + } + state.i = i + state.byte = byte +} + +// `g` is required to iterate all matches with exec; `d` is required for the +// per-group indices used by `capturesAll`. +module.exports.compile = function (pattern) { + return new RegExp(pattern, 'gd') +} + +module.exports.isMatch = function (re, hay) { + re.lastIndex = 0 + const m = re.exec(hay) + re.lastIndex = 0 + return m !== null +} + +module.exports.findFirst = function (re, hay) { + re.lastIndex = 0 + const m = re.exec(hay) + re.lastIndex = 0 + if (m === null) return new Int32Array(0) + const state = { i: 0, byte: 0 } + walkTo(hay, state, m.index) + const byteStart = state.byte + walkTo(hay, state, m.index + m[0].length) + return Int32Array.of(byteStart, state.byte) +} + +// Flat [s0, e0, s1, e1, ...] of UTF-8 byte offsets. One forward pass through +// the haystack, threading through all matches in order — no offset map. +module.exports.findAll = function (re, hay) { + re.lastIndex = 0 + const out = [] + const state = { i: 0, byte: 0 } + let m + while ((m = re.exec(hay)) !== null) { + walkTo(hay, state, m.index) + const byteStart = state.byte + walkTo(hay, state, m.index + m[0].length) + out.push(byteStart, state.byte) + if (m[0].length === 0) re.lastIndex++ // zero-width match: force progress + } + re.lastIndex = 0 + return Int32Array.from(out) +} + +// Flat [groupCount, matchCount, s0_0, e0_0, ..., s0_g, e0_g, s1_0, e1_0, ...] +// with (-1, -1) for absent groups. `groupCount` is the total slots per match +// (1 + number of capture groups). Empty result encodes as [0, 0]. +module.exports.capturesAll = function (re, hay) { + re.lastIndex = 0 + const out = [0, 0] // header slots; filled in after the scan + const state = { i: 0, byte: 0 } + const len = hay.length + let groupCount = 0 + let matchCount = 0 + let m + while ((m = re.exec(hay)) !== null) { + if (matchCount === 0) groupCount = m.length + matchCount++ + const matchStart = m.index + const matchEnd = matchStart + m[0].length + walkTo(hay, state, matchStart) + // Group endpoints within a match may be out of order (nested groups), so + // resolve them via a small local map covering just this match's range. + const local = new Uint32Array(matchEnd - matchStart + 1) + let i = state.i + let byte = state.byte + while (i < matchEnd) { + local[i - matchStart] = byte + byte += cuBytes(hay, i, len) + i += 1 + } + local[matchEnd - matchStart] = byte + state.i = i + state.byte = byte + for (let g = 0; g < groupCount; g++) { + const idx = m.indices[g] + if (idx === undefined) out.push(-1, -1) + else out.push(local[idx[0] - matchStart], local[idx[1] - matchStart]) + } + if (m[0].length === 0) re.lastIndex++ + } + re.lastIndex = 0 + out[0] = groupCount + out[1] = matchCount + return Int32Array.from(out) +} diff --git a/crates/capabilities/src/regex.rs b/crates/capabilities/src/regex.rs new file mode 100644 index 00000000..c0a4fa01 --- /dev/null +++ b/crates/capabilities/src/regex.rs @@ -0,0 +1,153 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Wasm implementation of [`RegexCapability`] backed by Node.js's native +//! `RegExp`. +//! +//! The compiled `RegExp` object is returned to Rust as a `js_sys::RegExp` +//! (a `JsValue` newtype). +//! +//! The JS side (`regex.js`) is responsible for translating JS `RegExp.exec` +//! results (UTF-16 code-unit indices) to UTF-8 byte offsets before returning +//! them. + +use js_sys::{self, Int32Array}; +use wasm_bindgen::prelude::*; + +use libdd_capabilities::regex::{Captures, Match, RegexCapability, RegexError}; + +#[wasm_bindgen(module = "/src/regex.js")] +extern "C" { + #[wasm_bindgen(js_name = "compile", catch)] + fn js_compile(pattern: &str) -> Result; + + #[wasm_bindgen(js_name = "isMatch")] + fn js_is_match(re: &js_sys::RegExp, haystack: &str) -> bool; + + #[wasm_bindgen(js_name = "findFirst")] + fn js_find_first(re: &js_sys::RegExp, haystack: &str) -> Int32Array; + + #[wasm_bindgen(js_name = "findAll")] + fn js_find_all(re: &js_sys::RegExp, haystack: &str) -> Int32Array; + + #[wasm_bindgen(js_name = "capturesAll")] + fn js_captures_all(re: &js_sys::RegExp, haystack: &str) -> Int32Array; +} + +// Pattern-string-preserving wrapper. `js_sys::RegExp::source` returns the +// pattern as a JS string, but the trait's `pattern()` returns `&str` borrowed +// from the handle, so we keep an owned copy on the Rust side. +#[derive(Clone, Debug)] +pub struct WasmRegexHandle { + re: js_sys::RegExp, + pattern: String, +} + +#[derive(Clone, Debug)] +pub struct WasmRegexCapability; + +impl RegexCapability for WasmRegexCapability { + type Handle = WasmRegexHandle; + + fn compile(pattern: &str) -> Result { + js_compile(pattern) + .map(|re| WasmRegexHandle { + re, + pattern: pattern.to_owned(), + }) + .map_err(|e| RegexError::InvalidPattern { + pattern: pattern.to_owned(), + message: js_error_message(&e), + }) + } + + fn is_match(handle: &Self::Handle, haystack: &str) -> bool { + js_is_match(&handle.re, haystack) + } + + fn find(handle: &Self::Handle, haystack: &str) -> Option { + let arr = js_find_first(&handle.re, haystack); + if arr.length() < 2 { + return None; + } + let mut buf = [0i32; 2]; + arr.copy_to(&mut buf); + Some(Match { + start: buf[0] as usize, + end: buf[1] as usize, + }) + } + + fn find_all(handle: &Self::Handle, haystack: &str) -> Vec { + let arr = js_find_all(&handle.re, haystack); + let len = arr.length() as usize; + debug_assert!(len.is_multiple_of(2), "findAll returned odd-length array"); + let mut buf = vec![0i32; len]; + arr.copy_to(&mut buf); + buf.chunks_exact(2) + .map(|c| Match { + start: c[0] as usize, + end: c[1] as usize, + }) + .collect() + } + + fn captures(handle: &Self::Handle, haystack: &str) -> Option { + decode_captures(js_captures_all(&handle.re, haystack)) + .1 + .into_iter() + .next() + } + + fn captures_all(handle: &Self::Handle, haystack: &str) -> Vec { + decode_captures(js_captures_all(&handle.re, haystack)).1 + } + + fn pattern(handle: &Self::Handle) -> &str { + &handle.pattern + } +} + +// Decode the [groupCount, matchCount, s0_0, e0_0, ...] flat layout produced by +// `capturesAll`. `-1` sentinels become `None` groups. Returns (group_count, +// matches). +fn decode_captures(arr: Int32Array) -> (usize, Vec) { + let len = arr.length() as usize; + if len < 2 { + return (0, Vec::new()); + } + let mut buf = vec![0i32; len]; + arr.copy_to(&mut buf); + let group_count = buf[0] as usize; + let match_count = buf[1] as usize; + if group_count == 0 || match_count == 0 { + return (group_count, Vec::new()); + } + let mut out = Vec::with_capacity(match_count); + let stride = group_count * 2; + for m in 0..match_count { + let base = 2 + m * stride; + let mut groups = Vec::with_capacity(group_count); + for g in 0..group_count { + let s = buf[base + g * 2]; + let e = buf[base + g * 2 + 1]; + groups.push(if s < 0 { + None + } else { + Some(Match { + start: s as usize, + end: e as usize, + }) + }); + } + out.push(Captures { groups }); + } + (group_count, out) +} + +fn js_error_message(err: &JsValue) -> String { + js_sys::Reflect::get(err, &JsValue::from_str("message")) + .ok() + .and_then(|v| v.as_string()) + .unwrap_or_else(|| format!("{err:?}")) +} diff --git a/crates/pipeline/Cargo.toml b/crates/pipeline/Cargo.toml index 29fc2722..ad0e7538 100644 --- a/crates/pipeline/Cargo.toml +++ b/crates/pipeline/Cargo.toml @@ -14,13 +14,13 @@ js-sys = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1" libdatadog-nodejs-capabilities = { path = "../capabilities" } -libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", rev = "3081603d3c74f209be4e3be951f78a1a7469397f" } -libdd-common = { git = "https://github.com/DataDog/libdatadog.git", rev = "3081603d3c74f209be4e3be951f78a1a7469397f", default-features = false } -libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", rev = "3081603d3c74f209be4e3be951f78a1a7469397f", default-features = false } -libdd-trace-utils = { git = "https://github.com/DataDog/libdatadog.git", rev = "3081603d3c74f209be4e3be951f78a1a7469397f", default-features = false, features = ["change-buffer"] } -libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog.git", rev = "3081603d3c74f209be4e3be951f78a1a7469397f", default-features = false } -libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", rev = "3081603d3c74f209be4e3be951f78a1a7469397f", default-features = false } -libdd-shared-runtime = { git = "https://github.com/DataDog/libdatadog.git", rev = "3081603d3c74f209be4e3be951f78a1a7469397f", default-features = false } +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", rev = "3cdb794e501bc37e52090b032f0b4be116f9a26f" } +libdd-common = { git = "https://github.com/DataDog/libdatadog.git", rev = "3cdb794e501bc37e52090b032f0b4be116f9a26f", default-features = false } +libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", rev = "3cdb794e501bc37e52090b032f0b4be116f9a26f", default-features = false } +libdd-trace-utils = { git = "https://github.com/DataDog/libdatadog.git", rev = "3cdb794e501bc37e52090b032f0b4be116f9a26f", default-features = false, features = ["change-buffer"] } +libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog.git", rev = "3cdb794e501bc37e52090b032f0b4be116f9a26f", default-features = false } +libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", rev = "3cdb794e501bc37e52090b032f0b4be116f9a26f", default-features = false } +libdd-shared-runtime = { git = "https://github.com/DataDog/libdatadog.git", rev = "3cdb794e501bc37e52090b032f0b4be116f9a26f", default-features = false } web-time = "1" rmp-serde = "1" bytes = "1" diff --git a/test/pipeline.js b/test/pipeline.js index 59511fef..298e5638 100644 --- a/test/pipeline.js +++ b/test/pipeline.js @@ -1154,7 +1154,10 @@ describe('pipeline', { skip }, () => { try { for (let i = 0; i < 15_000; i++) { const span = ns.createSpan() - span.name = 'stats-span' + // Vary `name` (not per-field-limited); varying only `resource` + // hits the per-field resource cap (1024) before the whole-key + // limit (7000) and never overflows. + span.name = `stats-span-${i}` span.service = 'stats-svc' span.resource = `/stats/${i}` span.type = 'web' diff --git a/test/regex.js b/test/regex.js new file mode 100644 index 00000000..00ee253b --- /dev/null +++ b/test/regex.js @@ -0,0 +1,118 @@ +'use strict' + +const { describe, it } = require('node:test') +const assert = require('node:assert') + +const regex = require('../crates/capabilities/src/regex') + +const utf8Len = s => Buffer.byteLength(s, 'utf8') + +describe('regex', () => { + it('findFirst — pure ASCII', () => { + assert.deepStrictEqual([...regex.findFirst(regex.compile(String.raw`\d+`), 'abc123def')], [3, 6]) + }) + + it('findFirst — no match returns empty', () => { + assert.strictEqual(regex.findFirst(regex.compile('z'), 'abc').length, 0) + }) + + it('findFirst — 2-byte UTF-8 (é) precedes the match', () => { + const hay = 'café☕end' + const [s, e] = regex.findFirst(regex.compile('end'), hay) + assert.strictEqual(s, utf8Len('café☕')) + assert.strictEqual(e, utf8Len('café☕end')) + }) + + it('findFirst — 3-byte CJK haystack', () => { + const hay = 'hello 你好 world' + assert.deepStrictEqual([...regex.findFirst(regex.compile('你好'), hay)], + [utf8Len('hello '), utf8Len('hello 你好')]) + }) + + it('findFirst — match spans a surrogate pair (4-byte UTF-8)', () => { + // 😀 = U+1F600, 4 UTF-8 bytes. + const hay = 'abc😀def' + assert.deepStrictEqual([...regex.findFirst(regex.compile('😀'), hay)], + [3, 3 + 4]) + }) + + it('findFirst — ZWJ family emoji (mixed surrogate pairs + 3-byte ZWJ)', () => { + // 👨‍👩‍👧 = 👨 (4B) + ZWJ (3B) + 👩 (4B) + ZWJ (3B) + 👧 (4B) = 18 UTF-8 bytes. + const family = '👨‍👩‍👧' + const hay = 'hi ' + family + ' bye' + assert.strictEqual(utf8Len(family), 18) + assert.deepStrictEqual([...regex.findFirst(regex.compile('bye'), hay)], + [3 + 18 + 1, 3 + 18 + 1 + 3]) + }) + + it('findFirst — haystack starts with non-ASCII (empty ASCII prefix)', () => { + const hay = '🌸🌸abc' + assert.deepStrictEqual([...regex.findFirst(regex.compile('abc'), hay)], + [8, 11]) + }) + + it('findFirst — combining mark does not confuse offsets', () => { + // "e" + U+0301 (combining acute) = 3 UTF-8 bytes. + const hay = 'e\u0301xt' + assert.deepStrictEqual([...regex.findFirst(regex.compile('xt'), hay)], + [3, 5]) + }) + + it('findAll — multiple matches interleaved with multi-byte chars', () => { + // 你 is 3 bytes. + const hay = 'a你b你c' + assert.deepStrictEqual([...regex.findAll(regex.compile('[abc]'), hay)], + [0, 1, 4, 5, 8, 9]) + }) + + it('findAll — zero-width lookahead produces empty matches', () => { + assert.deepStrictEqual([...regex.findAll(regex.compile('(?=a)'), 'aaa')], + [0, 0, 1, 1, 2, 2]) + }) + + it('findAll — no matches on non-ASCII input', () => { + assert.strictEqual(regex.findAll(regex.compile('z'), '你好世界').length, 0) + }) + + it('findAll — zero-width matches across a surrogate pair do not collapse', () => { + // "a😀b": 'a'=byte 0, 😀=bytes 1-4, 'b'=byte 5. + // (?=) fires at every code-unit boundary (5 positions in JS). + // The mid-surrogate position must not collapse onto the byte offset of 'b'. + const hay = 'a😀b' + const out = [...regex.findAll(regex.compile('(?=)'), hay)] + assert.strictEqual(out.length, 10, 'expected 5 zero-width matches') + assert.deepStrictEqual(out.slice(0, 4), [0, 0, 1, 1], 'positions 0 and 1') + assert.deepStrictEqual(out.slice(6), [5, 5, 6, 6], 'positions 3 and 4') + assert.notStrictEqual(out[4], out[6], + `mid-pair position 2 (byte=${out[4]}) collapsed onto position 3 (byte=${out[6]})`) + }) + + it('isMatch — true and false', () => { + assert.strictEqual(regex.isMatch(regex.compile('好'), '你好'), true) + assert.strictEqual(regex.isMatch(regex.compile('z'), '你好'), false) + }) + + it('capturesAll — nested groups with non-monotonic endpoints', () => { + // "(a(b))(c)" on "abc": g0=[0,3], g1=[0,2], g2=[1,2], g3=[2,3]. + assert.deepStrictEqual([...regex.capturesAll(regex.compile('(a(b))(c)'), 'abc')], + [4, 1, 0, 3, 0, 2, 1, 2, 2, 3]) + }) + + it('capturesAll — absent group under alternation encodes as (-1, -1)', () => { + assert.deepStrictEqual([...regex.capturesAll(regex.compile('(a)|(b)'), 'ab')], + [3, 2, 0, 1, 0, 1, -1, -1, 1, 2, -1, -1, 1, 2]) + }) + + it('capturesAll — group spanning multi-byte content', () => { + const hay = 'prefix日本postfix' + const bStart = utf8Len('prefix') + const bEnd = utf8Len('prefix日本') + assert.deepStrictEqual([...regex.capturesAll(regex.compile('(日本)'), hay)], + [2, 1, bStart, bEnd, bStart, bEnd]) + }) + + it('capturesAll — no matches encodes as [0, 0]', () => { + assert.deepStrictEqual([...regex.capturesAll(regex.compile('z'), 'abc')], + [0, 0]) + }) +})