Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions src/compiler/value/arithmetic.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#![deny(clippy::arithmetic_side_effects)]

use std::ops::{Add, Mul, Rem};

use crate::compiler::{
value::{Kind, VrlValueConvert},
Expand Down Expand Up @@ -68,6 +67,33 @@ fn safe_sub(lhv: f64, rhv: f64) -> Option<Value> {
}
}

fn safe_add(lhv: f64, rhv: f64) -> Option<Value> {
let result = lhv + rhv;
if result.is_nan() {
None
} else {
Some(Value::from_f64_or_zero(result))
}
}

fn safe_mul(lhv: f64, rhv: f64) -> Option<Value> {
let result = lhv * rhv;
if result.is_nan() {
None
} else {
Some(Value::from_f64_or_zero(result))
}
}

fn safe_rem(lhv: f64, rhv: f64) -> Option<Value> {
let result = lhv % rhv;
if result.is_nan() {
None
} else {
Some(Value::from_f64_or_zero(result))
}
}

impl VrlValueArithmetic for Value {
/// Similar to [`std::ops::Mul`], but fallible (e.g. `TryMul`).
fn try_mul(self, rhs: Self) -> Result<Self, ValueError> {
Expand All @@ -90,7 +116,7 @@ impl VrlValueArithmetic for Value {
}
Value::Float(lhv) => {
let rhs = rhs.try_into_f64().map_err(|_| err())?;
lhv.mul(rhs).into()
safe_mul(*lhv, rhs).ok_or_else(err)?
}
Value::Bytes(lhv) if rhs.is_integer() => {
Bytes::from(lhv.repeat(as_usize(rhs.try_integer()?))).into()
Expand Down Expand Up @@ -134,7 +160,8 @@ impl VrlValueArithmetic for Value {
let rhs = rhs
.try_into_f64()
.map_err(|_| ValueError::Add(Kind::float(), rhs.kind()))?;
lhs.add(rhs).into()
safe_add(*lhs, rhs)
.ok_or(ValueError::Add(Kind::float(), Kind::float()))?
}
(lhs @ Value::Bytes(_), Value::Null) => lhs,
(Value::Bytes(lhs), Value::Bytes(rhs)) => {
Expand Down Expand Up @@ -230,7 +257,7 @@ impl VrlValueArithmetic for Value {
}
Value::Float(lhv) => {
let rhv = rhs.try_into_f64().map_err(|_| err())?;
lhv.rem(rhv).into()
safe_rem(*lhv, rhv).ok_or_else(err)?
}
_ => return Err(err()),
};
Expand Down
1 change: 1 addition & 0 deletions src/parser/lex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,7 @@ fn unescape_string_literal(mut s: &str) -> String {
b't' => '\t',
b'0' => '\0',
b'{' => '{',
b'}' => '}',
_ => unimplemented!("invalid escape"),
};

Expand Down
35 changes: 19 additions & 16 deletions src/parsing/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,22 +162,25 @@ fn process_node(node: Node, config: &ParseXmlConfig) -> Value {
_ => match node.children().count() {
// For a single node, 'flatten' the object if necessary.
1 => {
// Expect a single element.
let node = node.children().next().expect("expected 1 XML node");

// If the node is an element, treat it as an object.
if node.is_element() {
let mut map = BTreeMap::new();

map.insert(
node.tag_name().name().to_string().into(),
process_node(node, config),
);

Value::Object(map)
} else {
// Otherwise, 'flatten' the object by continuing processing.
process_node(node, config)
// Skip non-element/non-text nodes (e.g. comments, PIs) to prevent
// passing them to process_node which cannot handle them.
let child = node
.children()
.find(|n| n.is_element() || n.is_text());
match child {
Some(node) if node.is_element() => {
let mut map = BTreeMap::new();

map.insert(
node.tag_name().name().to_string().into(),
process_node(node, config),
);

Value::Object(map)
}
Some(node) => process_node(node, config),
// Only child is a comment or PI — treat as empty element.
None => Value::Object(recurse(node)),
}
}
// For 2+ nodes, expand.
Expand Down
24 changes: 23 additions & 1 deletion src/stdlib/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::compiler::prelude::*;
#[allow(clippy::cast_possible_wrap)]
fn find(value: Value, pattern: Value, from: Option<Value>) -> Resolved {
let from = match from {
Some(value) => value.try_integer()?,
Some(value) => value.try_integer()?.max(0),
None => 0,
} as usize;

Expand Down Expand Up @@ -75,6 +75,9 @@ struct FindFn {

impl FindFn {
fn find_regex_in_str(value: &str, regex: ValueRegex, offset: usize) -> Option<usize> {
if offset > value.len() {
return None;
}
regex.find_at(value, offset).map(|found| found.start())
}

Expand Down Expand Up @@ -178,5 +181,24 @@ mod tests {
want: Err("expected string or regex, got integer"),
tdef: TypeDef::integer().infallible(),
}

// OBE-10722: negative from wraps to huge usize, panics in regex::find_at
negative_from_string {
args: func_args![value: "foobar", pattern: "bar", from: -1_i64],
want: Ok(value!(3)),
tdef: TypeDef::integer().infallible(),
}

negative_from_regex {
args: func_args![value: "foobar", pattern: Value::Regex(Regex::new("bar").unwrap().into()), from: -10_i64],
want: Ok(value!(3)),
tdef: TypeDef::integer().infallible(),
}

from_past_end {
args: func_args![value: "foobar", pattern: Value::Regex(Regex::new("bar").unwrap().into()), from: 100_i64],
want: Ok(value!(-1)),
tdef: TypeDef::integer().infallible(),
}
];
}
61 changes: 50 additions & 11 deletions src/stdlib/format_number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ fn format_number(
) -> Resolved {
let value: Decimal = match value {
Value::Integer(v) => v.into(),
Value::Float(v) => Decimal::from_f64(*v).expect("not NaN"),
Value::Float(v) => Decimal::from_f64(*v)
.ok_or("cannot convert float to decimal: value is infinite or out of range")?,
value => {
return Err(ValueError::Expected {
got: value.kind(),
Expand Down Expand Up @@ -39,8 +40,14 @@ fn format_number(
debug_assert!(parts.len() <= 2);
// Manipulate fractional part based on configuration.
match scale {
Some(i) if i < 0 => {
return Err(format!("scale must be non-negative, got {i}").into());
}
Some(0) => parts.truncate(1),
Some(i) => {
if i > 1024 {
return Err(format!("scale must not exceed 1024, got {i}").into());
}
let i = i as usize;

if parts.len() == 1 {
Expand Down Expand Up @@ -173,7 +180,7 @@ impl FunctionExpression for FormatNumberFn {
}

fn type_def(&self, _: &state::TypeState) -> TypeDef {
TypeDef::bytes().infallible()
TypeDef::bytes().fallible()
}
}

Expand All @@ -188,14 +195,14 @@ mod tests {
number {
args: func_args![value: 1234.567],
want: Ok(value!("1234.567")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}

precision {
args: func_args![value: 1234.567,
scale: 2],
want: Ok(value!("1234.56")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}


Expand All @@ -204,7 +211,7 @@ mod tests {
scale: 2,
decimal_separator: ","],
want: Ok(value!("1234,56")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}

more_separators {
Expand All @@ -213,7 +220,7 @@ mod tests {
decimal_separator: ",",
grouping_separator: " "],
want: Ok(value!("1 234,56")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}

big_number {
Expand All @@ -222,34 +229,66 @@ mod tests {
decimal_separator: ",",
grouping_separator: "."],
want: Ok(value!("11.222.333.444,567")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}

integer {
args: func_args![value: 100.0],
want: Ok(value!("100")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}

integer_decimals {
args: func_args![value: 100.0,
scale: 2],
want: Ok(value!("100.00")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}

float_no_decimals {
args: func_args![value: 123.45,
scale: 0],
want: Ok(value!("123")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}

integer_no_decimals {
args: func_args![value: 12345,
scale: 2],
want: Ok(value!("12345.00")),
tdef: TypeDef::bytes().infallible(),
tdef: TypeDef::bytes().fallible(),
}

// OBE-10723: panic on ±∞ (float not representable as Decimal)
float_infinity {
args: func_args![value: f64::INFINITY],
want: Err("cannot convert float to decimal: value is infinite or out of range"),
tdef: TypeDef::bytes().fallible(),
}

float_neg_infinity {
args: func_args![value: f64::NEG_INFINITY],
want: Err("cannot convert float to decimal: value is infinite or out of range"),
tdef: TypeDef::bytes().fallible(),
}

// OBE-10724: OOM / panic on negative or huge scale
negative_scale {
args: func_args![value: 1.0, scale: -1_i64],
want: Err("scale must be non-negative, got -1"),
tdef: TypeDef::bytes().fallible(),
}

excessive_scale {
args: func_args![value: 1.0, scale: 1025_i64],
want: Err("scale must not exceed 1024, got 1025"),
tdef: TypeDef::bytes().fallible(),
}

max_allowed_scale {
args: func_args![value: 1.5, scale: 3_i64],
want: Ok(value!("1.500")),
tdef: TypeDef::bytes().fallible(),
}
];
}
10 changes: 7 additions & 3 deletions src/stdlib/parse_grok.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ mod non_wasm {

fn parse_grok(value: Value, pattern: Arc<grok::Pattern>) -> Resolved {
let bytes = value.try_bytes_utf8_lossy()?;
match pattern.match_against(&bytes) {
Some(matches) => {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
pattern.match_against(&bytes)
}));
match result {
Ok(Some(matches)) => {
let mut result = BTreeMap::new();

for (name, value) in &matches {
Expand All @@ -20,7 +23,8 @@ mod non_wasm {

Ok(Value::from(result))
}
None => Err("unable to parse input with grok pattern".into()),
Ok(None) => Err("unable to parse input with grok pattern".into()),
Err(_) => Err("grok pattern match failed: regex engine error".into()),
}
}

Expand Down
29 changes: 28 additions & 1 deletion src/stdlib/starts_with.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ impl Iterator for Chars<'_> {
if width == 1 {
self.pos += 1;
Some(Ok(self.bytes[self.pos - 1] as char))
} else if width == 0 || self.pos + width > self.bytes.len() {
// Invalid lead byte (width==0 for continuation/forbidden bytes) or truncated
// multi-byte sequence: yield the raw byte as an error and advance by one.
let byte = self.bytes[self.pos];
self.pos += 1;
Some(Err(byte))
} else {
let c = std::str::from_utf8(&self.bytes[self.pos..self.pos + width]);
match c {
Expand All @@ -31,8 +37,9 @@ impl Iterator for Chars<'_> {
Some(Ok(chr.chars().next().unwrap()))
}
Err(_) => {
let byte = self.bytes[self.pos];
self.pos += 1;
Some(Err(self.bytes[self.pos]))
Some(Err(byte))
}
}
}
Expand Down Expand Up @@ -165,6 +172,7 @@ impl FunctionExpression for StartsWithFn {
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;

test_function![
starts_with => StartsWith;
Expand Down Expand Up @@ -269,5 +277,24 @@ mod tests {
want: Ok(true),
tdef: TypeDef::boolean().infallible(),
}

// OBE-10733: stray continuation byte (0x80) causes width==0 → panic without the fix
invalid_utf8_lead_byte_case_insensitive {
args: func_args![value: Value::Bytes(Bytes::from(vec![0x80u8, b'a', b'b', b'c'])),
substring: "abc",
case_sensitive: false
],
want: Ok(false),
tdef: TypeDef::boolean().infallible(),
}

invalid_utf8_truncated_multibyte {
args: func_args![value: Value::Bytes(Bytes::from(vec![0xc3u8])),
substring: "a",
case_sensitive: false
],
want: Ok(false),
tdef: TypeDef::boolean().infallible(),
}
];
}
5 changes: 3 additions & 2 deletions src/value/value/crud/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ pub fn insert<'a, T: ValueCollection>(
if let Some(Value::Array(array)) = value.get_mut_value(key.borrow()) {
insert(array, index, path_iter, insert_value)
} else {
const MAX_ARRAY_CAPACITY: usize = 32_769;
let capacity = if index >= 0 {
(index as usize) + 1
((index as usize) + 1).min(MAX_ARRAY_CAPACITY)
} else {
(-index) as usize
((-index) as usize).min(MAX_ARRAY_CAPACITY)
};
let mut array = Vec::with_capacity(capacity);
let prev_value = insert(&mut array, index, path_iter, insert_value);
Expand Down
Loading