From 9a4b35a4cfb419c6a7f2f096b156e30aeed744ee Mon Sep 17 00:00:00 2001 From: Prabhakar Singh Date: Wed, 1 Jul 2026 22:29:01 -0700 Subject: [PATCH 1/6] fix: sanitize array elements in list_condition_expr to prevent SQL injection Closes #1688 The list_condition_expr function was interpolating inner_value directly into ARRAY[...] SQL expressions without any escaping or validation, allowing crafted input like `1] OR 1=1; --` to break out of the ARRAY context and inject arbitrary SQL. scalar_condition_expr already handled escaping correctly (single-quote escaping, numeric/boolean validation). This commit brings list_condition_expr to the same standard by adding a sanitize_array_elements helper that: - Splits the value by comma - Validates each element as numeric, boolean, or a properly single-quoted string - Rejects any element containing bracket characters `[` or `]` that could escape the ARRAY literal context - Returns an Err if any element fails validation --- src/alerts/alerts_utils.rs | 71 +++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index 25a9a53a5..4e0daff10 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -455,6 +455,66 @@ fn condition_to_expr(condition: &ConditionConfig) -> Result { } } +/// Sanitize and validate each element of a comma-separated array value intended +/// for interpolation into an SQL `ARRAY[...]` literal. +/// +/// Each element must be one of: +/// - A valid numeric literal (integer or float) +/// - A valid boolean literal (`true` or `false`) +/// - A single-quoted string with internal single quotes escaped as `''` +/// +/// Any element containing unquoted bracket characters `[` or `]` is rejected +/// outright to prevent escaping the `ARRAY[...]` context. +/// +/// Returns the sanitized, comma-joined string ready for interpolation, or an +/// `Err` describing the first offending element. +fn sanitize_array_elements(value: &str) -> Result { + let sanitized: Result, String> = value + .split(',') + .map(|elem| { + let elem = elem.trim(); + + // Reject bracket characters that could escape the ARRAY[...] context + if elem.contains('[') || elem.contains(']') { + return Err(format!( + "Invalid array element: '{elem}' contains bracket characters" + )); + } + + // Numeric literal — pass through as-is + if elem.parse::().is_ok() { + return Ok(elem.to_string()); + } + + // Boolean literal — pass through as-is + if elem.parse::().is_ok() { + return Ok(elem.to_string()); + } + + // String literal — must be single-quoted; strip outer quotes and re-escape internals + if elem.starts_with('\'') && elem.ends_with('\'') && elem.len() >= 2 { + let inner = &elem[1..elem.len() - 1]; + let escaped = inner.replace('\'', "''"); + return Ok(format!("'{escaped}'")); + } + + // Bare unquoted string — treat as a string literal and quote it safely + if elem + .chars() + .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.') + { + return Ok(format!("'{}'", elem.replace('\'', "''"))); + } + + Err(format!( + "Invalid array element: '{elem}' is not a valid numeric, boolean, or quoted string literal" + )) + }) + .collect(); + + Ok(sanitized?.join(", ")) +} + fn list_condition_expr( column: &str, operator: &WhereConfigOperator, @@ -467,15 +527,18 @@ fn list_condition_expr( .and_then(|s| s.strip_suffix(']')) .unwrap_or(value); + // Sanitize each element before interpolating into the SQL ARRAY literal + let safe_value = sanitize_array_elements(inner_value)?; + match operator { WhereConfigOperator::Contains => { - Ok(format!("array_has_all(\"{column}\", ARRAY[{inner_value}])")) + Ok(format!("array_has_all(\"{column}\", ARRAY[{safe_value}])")) } WhereConfigOperator::DoesNotContain => Ok(format!( - "NOT array_has_all(\"{column}\", ARRAY[{inner_value}])" + "NOT array_has_all(\"{column}\", ARRAY[{safe_value}])" )), - WhereConfigOperator::Equal => Ok(format!("\"{column}\" = ARRAY[{inner_value}]")), - WhereConfigOperator::NotEqual => Ok(format!("\"{column}\" != ARRAY[{inner_value}]")), + WhereConfigOperator::Equal => Ok(format!("\"{column}\" = ARRAY[{safe_value}]")), + WhereConfigOperator::NotEqual => Ok(format!("\"{column}\" != ARRAY[{safe_value}]")), _ => Err(format!( "Operator '{operator}' is not supported for list type columns" )), From 634f3581ccee85e9b50f3f86d45ff71b973cf2db Mon Sep 17 00:00:00 2001 From: Prabhakar Singh Date: Wed, 1 Jul 2026 22:41:25 -0700 Subject: [PATCH 2/6] fix: relax over-restrictive allowlist in sanitize_array_elements The bare unquoted string branch was rejecting valid values containing spaces, colons, slashes, etc. due to an alphanumeric-only allowlist. This is inconsistent with scalar_condition_expr which accepts any string and simply escapes single quotes. The bracket character check at the top already covers the actual injection vector, making the allowlist redundant. Now mirrors scalar_condition_expr: escape single quotes and wrap in single quotes, no character allowlist. --- src/alerts/alerts_utils.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index 4e0daff10..b5779faf7 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -461,10 +461,12 @@ fn condition_to_expr(condition: &ConditionConfig) -> Result { /// Each element must be one of: /// - A valid numeric literal (integer or float) /// - A valid boolean literal (`true` or `false`) -/// - A single-quoted string with internal single quotes escaped as `''` +/// - A single-quoted string (internal single quotes escaped as `''`) +/// - A bare unquoted string (automatically wrapped in single quotes, mirrors +/// the behaviour of `scalar_condition_expr`) /// -/// Any element containing unquoted bracket characters `[` or `]` is rejected -/// outright to prevent escaping the `ARRAY[...]` context. +/// Any element containing bracket characters `[` or `]` is rejected outright +/// to prevent escaping the `ARRAY[...]` context, regardless of quoting. /// /// Returns the sanitized, comma-joined string ready for interpolation, or an /// `Err` describing the first offending element. @@ -474,7 +476,9 @@ fn sanitize_array_elements(value: &str) -> Result { .map(|elem| { let elem = elem.trim(); - // Reject bracket characters that could escape the ARRAY[...] context + // Reject bracket characters that could escape the ARRAY[...] context. + // This is the primary injection guard; the quoting logic below is + // consistent with scalar_condition_expr and does not rely on this check. if elem.contains('[') || elem.contains(']') { return Err(format!( "Invalid array element: '{elem}' contains bracket characters" @@ -491,24 +495,18 @@ fn sanitize_array_elements(value: &str) -> Result { return Ok(elem.to_string()); } - // String literal — must be single-quoted; strip outer quotes and re-escape internals + // Single-quoted string — strip outer quotes, re-escape internals, re-wrap. + // Mirrors how scalar_condition_expr handles the Some(_) column_type branch. if elem.starts_with('\'') && elem.ends_with('\'') && elem.len() >= 2 { let inner = &elem[1..elem.len() - 1]; let escaped = inner.replace('\'', "''"); return Ok(format!("'{escaped}'")); } - // Bare unquoted string — treat as a string literal and quote it safely - if elem - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.') - { - return Ok(format!("'{}'", elem.replace('\'', "''"))); - } - - Err(format!( - "Invalid array element: '{elem}' is not a valid numeric, boolean, or quoted string literal" - )) + // Bare unquoted string — escape single quotes and wrap in single quotes. + // Mirrors the ValueType::String arm in scalar_condition_expr (None branch): + // format!("'{}'", val.replace("'", "''")) + Ok(format!("'{}'", elem.replace('\'', "''"))) }) .collect(); From 9b63db9fd02a228292c32cef249a924ce8b7ebd0 Mon Sep 17 00:00:00 2001 From: Prabhakar Singh Date: Wed, 1 Jul 2026 22:46:25 -0700 Subject: [PATCH 3/6] test: add unit tests for sanitize_array_elements and list_condition_expr --- src/alerts/alerts_utils.rs | 172 +++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index b5779faf7..65287aec7 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -638,3 +638,175 @@ impl Display for ValueType { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::alerts::WhereConfigOperator; + + // ------------------------------------------------------------------------- + // sanitize_array_elements — happy-path cases + // ------------------------------------------------------------------------- + + #[test] + fn test_sanitize_numeric_elements() { + // Plain integers and floats should pass through unchanged + assert_eq!(sanitize_array_elements("1, 2, 3").unwrap(), "1, 2, 3"); + assert_eq!( + sanitize_array_elements("3.14, 2.71").unwrap(), + "3.14, 2.71" + ); + } + + #[test] + fn test_sanitize_boolean_elements() { + assert_eq!( + sanitize_array_elements("true, false").unwrap(), + "true, false" + ); + } + + #[test] + fn test_sanitize_bare_string_elements() { + // Bare strings must be wrapped in single quotes + assert_eq!(sanitize_array_elements("foo").unwrap(), "'foo'"); + assert_eq!( + sanitize_array_elements("foo, bar").unwrap(), + "'foo', 'bar'" + ); + } + + #[test] + fn test_sanitize_pre_quoted_string_elements() { + // Pre-quoted strings should be accepted and returned normalised + assert_eq!(sanitize_array_elements("'hello'").unwrap(), "'hello'"); + } + + #[test] + fn test_sanitize_mixed_elements() { + // Mixed numeric + string values in one array + assert_eq!( + sanitize_array_elements("42, foo, 3.14").unwrap(), + "42, 'foo', 3.14" + ); + } + + // ------------------------------------------------------------------------- + // sanitize_array_elements — single-quote injection + // ------------------------------------------------------------------------- + + #[test] + fn test_sanitize_bare_string_with_single_quote_injection() { + // A bare string containing a single quote must have it escaped as '' + // so it cannot break out of the surrounding ARRAY[...] literal. + let input = "foo' OR '1'='1"; + let result = sanitize_array_elements(input).unwrap(); + assert_eq!(result, "'foo'' OR ''1''=''1'"); + // The output must not contain an unescaped lone single quote + assert!(!result.contains("' OR '")); + } + + #[test] + fn test_sanitize_pre_quoted_string_with_internal_single_quote() { + // A pre-quoted string whose interior contains a single quote must + // have that quote doubled before re-wrapping. + let input = "'it''s fine'"; + let result = sanitize_array_elements(input).unwrap(); + assert_eq!(result, "'it''''s fine'"); + } + + // ------------------------------------------------------------------------- + // sanitize_array_elements — bracket injection (primary guard) + // ------------------------------------------------------------------------- + + #[test] + fn test_sanitize_rejects_element_with_open_bracket() { + let result = sanitize_array_elements("foo[bar"); + assert!(result.is_err(), "Expected Err for element containing '['"); + assert!(result.unwrap_err().contains("bracket characters")); + } + + #[test] + fn test_sanitize_rejects_element_with_close_bracket() { + let result = sanitize_array_elements("foo]bar"); + assert!(result.is_err(), "Expected Err for element containing ']'"); + } + + #[test] + fn test_sanitize_rejects_injection_via_brackets() { + // Classic injection: close the ARRAY, inject SQL, re-open + let malicious = "1] OR 1=1 --"; + let result = sanitize_array_elements(malicious); + assert!( + result.is_err(), + "Bracket injection attempt should be rejected" + ); + } + + // ------------------------------------------------------------------------- + // list_condition_expr — output shape + // ------------------------------------------------------------------------- + + #[test] + fn test_list_condition_expr_contains_numeric() { + let result = + list_condition_expr("tags", &WhereConfigOperator::Contains, "1, 2, 3").unwrap(); + assert_eq!(result, "array_has_all(\"tags\", ARRAY[1, 2, 3])"); + } + + #[test] + fn test_list_condition_expr_contains_strings() { + let result = + list_condition_expr("tags", &WhereConfigOperator::Contains, "foo, bar").unwrap(); + assert_eq!(result, "array_has_all(\"tags\", ARRAY['foo', 'bar'])"); + } + + #[test] + fn test_list_condition_expr_strips_outer_brackets() { + // Input already wrapped in [] — should strip and sanitize correctly + let result = + list_condition_expr("tags", &WhereConfigOperator::Equal, "[1, 2]").unwrap(); + assert_eq!(result, "\"tags\" = ARRAY[1, 2]"); + } + + #[test] + fn test_list_condition_expr_does_not_contain() { + let result = list_condition_expr( + "tags", + &WhereConfigOperator::DoesNotContain, + "foo", + ) + .unwrap(); + assert_eq!(result, "NOT array_has_all(\"tags\", ARRAY['foo'])"); + } + + #[test] + fn test_list_condition_expr_not_equal() { + let result = + list_condition_expr("tags", &WhereConfigOperator::NotEqual, "42").unwrap(); + assert_eq!(result, "\"tags\" != ARRAY[42]"); + } + + #[test] + fn test_list_condition_expr_rejects_injection() { + // Injection attempt via bracket characters must surface as an error + let result = list_condition_expr( + "tags", + &WhereConfigOperator::Contains, + "1] OR 1=1 --", + ); + assert!( + result.is_err(), + "Injection via bracket characters must be rejected" + ); + } + + #[test] + fn test_list_condition_expr_unsupported_operator() { + // Operators that make no sense on list columns should return Err + let result = + list_condition_expr("tags", &WhereConfigOperator::GreaterThan, "1"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not supported")); + } +} From 75e9a70c4c0537e2b75c0d2cceae376199b01e97 Mon Sep 17 00:00:00 2001 From: Prabhakar Singh Date: Wed, 1 Jul 2026 23:07:28 -0700 Subject: [PATCH 4/6] test: fix false-negative assertion in single-quote injection test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contains("' OR '") check was a false negative — the correctly escaped output 'foo'' OR ''1''=''1' still contains that substring as part of the doubled quotes. The assert_eq on the exact escaped string is already sufficient to prove injection safety; drop the redundant contains check. --- src/alerts/alerts_utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index 65287aec7..0111fd3e2 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -699,11 +699,11 @@ mod tests { fn test_sanitize_bare_string_with_single_quote_injection() { // A bare string containing a single quote must have it escaped as '' // so it cannot break out of the surrounding ARRAY[...] literal. + // The assert_eq on the exact escaped output is sufficient to prove safety — + // the correctly escaped form 'foo'' OR ''1''=''1' is valid SQL and harmless. let input = "foo' OR '1'='1"; let result = sanitize_array_elements(input).unwrap(); assert_eq!(result, "'foo'' OR ''1''=''1'"); - // The output must not contain an unescaped lone single quote - assert!(!result.contains("' OR '")); } #[test] From 630d231b6e9bbef031537a915d012683e431fc4f Mon Sep 17 00:00:00 2001 From: Prabhakar Singh Date: Thu, 2 Jul 2026 07:56:47 -0700 Subject: [PATCH 5/6] fix: quote-aware splitting in sanitize_array_elements The previous value.split(',') broke on quoted elements that contained commas, e.g. 'New York, NY' was split into two invalid fragments. Replace it with a hand-rolled quote-aware splitter that: - Treats '' inside a quoted string as an escaped quote (not a close) - Only splits on commas that appear outside of single-quoted strings - Rejects unterminated quoted strings with a clear error - Preserves all existing trimming, bracket-rejection, and re-quoting behaviour unchanged New tests cover: comma inside a quoted element, comma inside a quoted element that also contains a doubled-quote escape, mixed quoted and numeric elements, and an unterminated quote error. --- src/alerts/alerts_utils.rs | 148 +++++++++++++++++++++++++++++++++---- 1 file changed, 132 insertions(+), 16 deletions(-) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index 0111fd3e2..c9f284023 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -455,6 +455,72 @@ fn condition_to_expr(condition: &ConditionConfig) -> Result { } } +/// Split a comma-separated list of array elements in a quote-aware manner. +/// +/// A plain `str::split(',')` breaks on commas that appear inside single-quoted +/// strings (e.g. `'New York, NY'`). This function walks the input char-by-char +/// and only treats a comma as a delimiter when it appears *outside* of a +/// single-quoted string. Doubled single quotes (`''`) inside a quoted string +/// are treated as an escaped quote character, not as a string terminator. +/// +/// Returns `Err` if the input ends while still inside an open quoted string. +fn split_array_elements(value: &str) -> Result, String> { + let mut elements = Vec::new(); + let mut in_quote = false; + let mut start = 0; + let chars: Vec = value.chars().collect(); + let len = chars.len(); + let mut i = 0; + + while i < len { + match chars[i] { + '\'' if in_quote => { + // Check for escaped quote: '' inside a quoted string + if i + 1 < len && chars[i + 1] == '\'' { + i += 2; // skip both quotes, stay in_quote + continue; + } + // Closing quote + in_quote = false; + } + '\'' => { + in_quote = true; + } + ',' if !in_quote => { + // Find byte offset for this char-level slice + let byte_start = value + .char_indices() + .nth(start) + .map(|(b, _)| b) + .unwrap_or(value.len()); + let byte_end = value + .char_indices() + .nth(i) + .map(|(b, _)| b) + .unwrap_or(value.len()); + elements.push(&value[byte_start..byte_end]); + start = i + 1; + } + _ => {} + } + i += 1; + } + + if in_quote { + return Err("Unterminated quoted string in array value".to_string()); + } + + // Push the final element + let byte_start = value + .char_indices() + .nth(start) + .map(|(b, _)| b) + .unwrap_or(value.len()); + elements.push(&value[byte_start..]); + + Ok(elements) +} + /// Sanitize and validate each element of a comma-separated array value intended /// for interpolation into an SQL `ARRAY[...]` literal. /// @@ -468,11 +534,16 @@ fn condition_to_expr(condition: &ConditionConfig) -> Result { /// Any element containing bracket characters `[` or `]` is rejected outright /// to prevent escaping the `ARRAY[...]` context, regardless of quoting. /// +/// The input is split using a quote-aware splitter so that commas inside +/// single-quoted strings (e.g. `'New York, NY'`) are not treated as delimiters. +/// /// Returns the sanitized, comma-joined string ready for interpolation, or an /// `Err` describing the first offending element. fn sanitize_array_elements(value: &str) -> Result { - let sanitized: Result, String> = value - .split(',') + let raw_elements = split_array_elements(value)?; + + let sanitized: Result, String> = raw_elements + .into_iter() .map(|elem| { let elem = elem.trim(); @@ -650,7 +721,6 @@ mod tests { #[test] fn test_sanitize_numeric_elements() { - // Plain integers and floats should pass through unchanged assert_eq!(sanitize_array_elements("1, 2, 3").unwrap(), "1, 2, 3"); assert_eq!( sanitize_array_elements("3.14, 2.71").unwrap(), @@ -668,7 +738,6 @@ mod tests { #[test] fn test_sanitize_bare_string_elements() { - // Bare strings must be wrapped in single quotes assert_eq!(sanitize_array_elements("foo").unwrap(), "'foo'"); assert_eq!( sanitize_array_elements("foo, bar").unwrap(), @@ -678,29 +747,67 @@ mod tests { #[test] fn test_sanitize_pre_quoted_string_elements() { - // Pre-quoted strings should be accepted and returned normalised assert_eq!(sanitize_array_elements("'hello'").unwrap(), "'hello'"); } #[test] fn test_sanitize_mixed_elements() { - // Mixed numeric + string values in one array assert_eq!( sanitize_array_elements("42, foo, 3.14").unwrap(), "42, 'foo', 3.14" ); } + // ------------------------------------------------------------------------- + // sanitize_array_elements — quote-aware comma splitting + // ------------------------------------------------------------------------- + + #[test] + fn test_sanitize_quoted_string_with_comma() { + // A quoted element containing a comma must be treated as one element, + // not split into two fragments. + let result = sanitize_array_elements("'New York, NY'").unwrap(); + assert_eq!(result, "'New York, NY'"); + } + + #[test] + fn test_sanitize_quoted_string_with_comma_mixed() { + // Quoted element with comma alongside plain numeric elements. + let result = sanitize_array_elements("'New York, NY', 42").unwrap(); + assert_eq!(result, "'New York, NY', 42"); + } + + #[test] + fn test_sanitize_quoted_string_with_comma_and_escaped_quote() { + // Quoted element containing both a doubled-quote escape and a comma. + // 'it''s here, really' -> inner is: it''s here, really + // After re-escaping the already-doubled quote: it''''s here, really + let result = sanitize_array_elements("'it''s here, really'").unwrap(); + assert_eq!(result, "'it''''s here, really'"); + } + + #[test] + fn test_sanitize_multiple_quoted_with_commas() { + // Two separate quoted elements each containing commas. + let result = + sanitize_array_elements("'hello, world', 'foo, bar'").unwrap(); + assert_eq!(result, "'hello, world', 'foo, bar'"); + } + + #[test] + fn test_sanitize_unterminated_quote_returns_error() { + // An unterminated quoted string should surface as an Err. + let result = sanitize_array_elements("'unterminated"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Unterminated")); + } + // ------------------------------------------------------------------------- // sanitize_array_elements — single-quote injection // ------------------------------------------------------------------------- #[test] fn test_sanitize_bare_string_with_single_quote_injection() { - // A bare string containing a single quote must have it escaped as '' - // so it cannot break out of the surrounding ARRAY[...] literal. - // The assert_eq on the exact escaped output is sufficient to prove safety — - // the correctly escaped form 'foo'' OR ''1''=''1' is valid SQL and harmless. let input = "foo' OR '1'='1"; let result = sanitize_array_elements(input).unwrap(); assert_eq!(result, "'foo'' OR ''1''=''1'"); @@ -708,8 +815,6 @@ mod tests { #[test] fn test_sanitize_pre_quoted_string_with_internal_single_quote() { - // A pre-quoted string whose interior contains a single quote must - // have that quote doubled before re-wrapping. let input = "'it''s fine'"; let result = sanitize_array_elements(input).unwrap(); assert_eq!(result, "'it''''s fine'"); @@ -734,7 +839,6 @@ mod tests { #[test] fn test_sanitize_rejects_injection_via_brackets() { - // Classic injection: close the ARRAY, inject SQL, re-open let malicious = "1] OR 1=1 --"; let result = sanitize_array_elements(malicious); assert!( @@ -763,7 +867,6 @@ mod tests { #[test] fn test_list_condition_expr_strips_outer_brackets() { - // Input already wrapped in [] — should strip and sanitize correctly let result = list_condition_expr("tags", &WhereConfigOperator::Equal, "[1, 2]").unwrap(); assert_eq!(result, "\"tags\" = ARRAY[1, 2]"); @@ -787,9 +890,23 @@ mod tests { assert_eq!(result, "\"tags\" != ARRAY[42]"); } + #[test] + fn test_list_condition_expr_quoted_element_with_comma() { + // Quoted element containing a comma should produce a single array entry. + let result = list_condition_expr( + "cities", + &WhereConfigOperator::Contains, + "'New York, NY'", + ) + .unwrap(); + assert_eq!( + result, + "array_has_all(\"cities\", ARRAY['New York, NY'])" + ); + } + #[test] fn test_list_condition_expr_rejects_injection() { - // Injection attempt via bracket characters must surface as an error let result = list_condition_expr( "tags", &WhereConfigOperator::Contains, @@ -803,7 +920,6 @@ mod tests { #[test] fn test_list_condition_expr_unsupported_operator() { - // Operators that make no sense on list columns should return Err let result = list_condition_expr("tags", &WhereConfigOperator::GreaterThan, "1"); assert!(result.is_err()); From f91ac79d7ab7f03075adf79135dc47f8b86c1fbc Mon Sep 17 00:00:00 2001 From: Prabhakar Singh Date: Thu, 2 Jul 2026 08:06:08 -0700 Subject: [PATCH 6/6] fix: resolve Clippy lints in split_array_elements Replace `.map(|(b, _)| b).unwrap_or(value.len())` with `.map_or(value.len(), |(b, _)| b)` in all three byte-offset lookups inside split_array_elements. Fixes: - clippy::unwrap_or_else_default (unwrap_or followed by a function call) - clippy::map_unwrap_or (.map().unwrap_or() on Option) --- src/alerts/alerts_utils.rs | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index c9f284023..98cd6e776 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -487,17 +487,14 @@ fn split_array_elements(value: &str) -> Result, String> { in_quote = true; } ',' if !in_quote => { - // Find byte offset for this char-level slice let byte_start = value .char_indices() .nth(start) - .map(|(b, _)| b) - .unwrap_or(value.len()); + .map_or(value.len(), |(b, _)| b); let byte_end = value .char_indices() .nth(i) - .map(|(b, _)| b) - .unwrap_or(value.len()); + .map_or(value.len(), |(b, _)| b); elements.push(&value[byte_start..byte_end]); start = i + 1; } @@ -514,8 +511,7 @@ fn split_array_elements(value: &str) -> Result, String> { let byte_start = value .char_indices() .nth(start) - .map(|(b, _)| b) - .unwrap_or(value.len()); + .map_or(value.len(), |(b, _)| b); elements.push(&value[byte_start..]); Ok(elements) @@ -567,7 +563,6 @@ fn sanitize_array_elements(value: &str) -> Result { } // Single-quoted string — strip outer quotes, re-escape internals, re-wrap. - // Mirrors how scalar_condition_expr handles the Some(_) column_type branch. if elem.starts_with('\'') && elem.ends_with('\'') && elem.len() >= 2 { let inner = &elem[1..elem.len() - 1]; let escaped = inner.replace('\'', "''"); @@ -575,8 +570,6 @@ fn sanitize_array_elements(value: &str) -> Result { } // Bare unquoted string — escape single quotes and wrap in single quotes. - // Mirrors the ValueType::String arm in scalar_condition_expr (None branch): - // format!("'{}'", val.replace("'", "''")) Ok(format!("'{}'", elem.replace('\'', "''"))) }) .collect(); @@ -764,31 +757,24 @@ mod tests { #[test] fn test_sanitize_quoted_string_with_comma() { - // A quoted element containing a comma must be treated as one element, - // not split into two fragments. let result = sanitize_array_elements("'New York, NY'").unwrap(); assert_eq!(result, "'New York, NY'"); } #[test] fn test_sanitize_quoted_string_with_comma_mixed() { - // Quoted element with comma alongside plain numeric elements. let result = sanitize_array_elements("'New York, NY', 42").unwrap(); assert_eq!(result, "'New York, NY', 42"); } #[test] fn test_sanitize_quoted_string_with_comma_and_escaped_quote() { - // Quoted element containing both a doubled-quote escape and a comma. - // 'it''s here, really' -> inner is: it''s here, really - // After re-escaping the already-doubled quote: it''''s here, really let result = sanitize_array_elements("'it''s here, really'").unwrap(); assert_eq!(result, "'it''''s here, really'"); } #[test] fn test_sanitize_multiple_quoted_with_commas() { - // Two separate quoted elements each containing commas. let result = sanitize_array_elements("'hello, world', 'foo, bar'").unwrap(); assert_eq!(result, "'hello, world', 'foo, bar'"); @@ -796,7 +782,6 @@ mod tests { #[test] fn test_sanitize_unterminated_quote_returns_error() { - // An unterminated quoted string should surface as an Err. let result = sanitize_array_elements("'unterminated"); assert!(result.is_err()); assert!(result.unwrap_err().contains("Unterminated")); @@ -892,7 +877,6 @@ mod tests { #[test] fn test_list_condition_expr_quoted_element_with_comma() { - // Quoted element containing a comma should produce a single array entry. let result = list_condition_expr( "cities", &WhereConfigOperator::Contains,