From 98b9839e3d1e2762d7a76df84939251b20218c83 Mon Sep 17 00:00:00 2001 From: Gavin Foley <6389719+gavinfoley@users.noreply.github.com> Date: Wed, 14 Dec 2022 08:54:13 -0500 Subject: [PATCH] Fix for escaping backslashes in interpolated strings (fixes #6737) (#7119) Co-authored-by: Gavin Foley --- crates/nu-parser/src/parser.rs | 62 +++--- crates/nu-parser/tests/test_parser.rs | 294 ++++++++++++++++++++++---- 2 files changed, 288 insertions(+), 68 deletions(-) diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index e61b9b112e..d993600589 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1639,42 +1639,52 @@ pub fn parse_string_interpolation( let mut token_start = start; let mut delimiter_stack = vec![]; + let mut consecutive_backslashes: usize = 0; + let mut b = start; while b != end { - if contents[b - start] == b'(' - && (if double_quote && (b - start) > 0 { - contents[b - start - 1] != b'\\' + let current_byte = contents[b - start]; + + if mode == InterpolationMode::String { + let preceding_consecutive_backslashes = consecutive_backslashes; + + let is_backslash = current_byte == b'\\'; + consecutive_backslashes = if is_backslash { + preceding_consecutive_backslashes + 1 } else { - true - }) - && mode == InterpolationMode::String - { - mode = InterpolationMode::Expression; - if token_start < b { - let span = Span::new(token_start, b); - let str_contents = working_set.get_span_contents(span); + 0 + }; - let str_contents = if double_quote { - let (str_contents, err) = unescape_string(str_contents, span); - error = error.or(err); + if current_byte == b'(' && (!double_quote || preceding_consecutive_backslashes % 2 == 0) + { + mode = InterpolationMode::Expression; + if token_start < b { + let span = Span::new(token_start, b); + let str_contents = working_set.get_span_contents(span); - str_contents - } else { - str_contents.to_vec() - }; + let str_contents = if double_quote { + let (str_contents, err) = unescape_string(str_contents, span); + error = error.or(err); - output.push(Expression { - expr: Expr::String(String::from_utf8_lossy(&str_contents).to_string()), - span, - ty: Type::String, - custom_completion: None, - }); - token_start = b; + str_contents + } else { + str_contents.to_vec() + }; + + output.push(Expression { + expr: Expr::String(String::from_utf8_lossy(&str_contents).to_string()), + span, + ty: Type::String, + custom_completion: None, + }); + token_start = b; + } } } + if mode == InterpolationMode::Expression { - let byte = contents[b - start]; + let byte = current_byte; if let Some(b'\'') = delimiter_stack.last() { if byte == b'\'' { delimiter_stack.pop(); diff --git a/crates/nu-parser/tests/test_parser.rs b/crates/nu-parser/tests/test_parser.rs index b3ca540cce..8856aebcd1 100644 --- a/crates/nu-parser/tests/test_parser.rs +++ b/crates/nu-parser/tests/test_parser.rs @@ -210,48 +210,6 @@ pub fn parse_binary_with_multi_byte_char() { } } -#[test] -pub fn parse_string() { - let engine_state = EngineState::new(); - let mut working_set = StateWorkingSet::new(&engine_state); - - let (block, err) = parse(&mut working_set, None, b"\"hello nushell\"", true, &[]); - - assert!(err.is_none()); - assert!(block.len() == 1); - let expressions = &block[0]; - assert!(expressions.len() == 1); - if let PipelineElement::Expression(_, expr) = &expressions[0] { - assert_eq!(expr.expr, Expr::String("hello nushell".to_string())) - } else { - panic!("Not an expression") - } -} - -#[test] -pub fn parse_escaped_string() { - let engine_state = EngineState::new(); - let mut working_set = StateWorkingSet::new(&engine_state); - - let (block, err) = parse( - &mut working_set, - None, - b"\"hello \\u006e\\u0075\\u0073hell\"", - true, - &[], - ); - - assert!(err.is_none()); - assert!(block.len() == 1); - let expressions = &block[0]; - assert!(expressions.len() == 1); - if let PipelineElement::Expression(_, expr) = &expressions[0] { - assert_eq!(expr.expr, Expr::String("hello nushell".to_string())) - } else { - panic!("Not an expression") - } -} - #[test] pub fn parse_call() { let engine_state = EngineState::new(); @@ -410,6 +368,258 @@ fn test_nothing_comparisson_neq() { )) } +mod string { + use super::*; + + #[test] + pub fn parse_string() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"\"hello nushell\"", true, &[]); + + assert!(err.is_none()); + assert!(block.len() == 1); + let expressions = &block[0]; + assert!(expressions.len() == 1); + if let PipelineElement::Expression(_, expr) = &expressions[0] { + assert_eq!(expr.expr, Expr::String("hello nushell".to_string())) + } else { + panic!("Not an expression") + } + } + + #[test] + pub fn parse_escaped_string() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse( + &mut working_set, + None, + b"\"hello \\u006e\\u0075\\u0073hell\"", + true, + &[], + ); + + assert!(err.is_none()); + assert!(block.len() == 1); + let expressions = &block[0]; + assert!(expressions.len() == 1); + if let PipelineElement::Expression(_, expr) = &expressions[0] { + assert_eq!(expr.expr, Expr::String("hello nushell".to_string())) + } else { + panic!("Not an expression") + } + } + + mod interpolation { + use nu_protocol::Span; + + use super::*; + + #[test] + pub fn parse_string_interpolation() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"$\"hello (39 + 3)\"", true, &[]); + + assert!(err.is_none()); + assert!(block.len() == 1); + + let expressions = &block[0]; + assert!(expressions.len() == 1); + + if let PipelineElement::Expression(_, expr) = &expressions[0] { + let subexprs: Vec<&Expr>; + match expr { + Expression { + expr: Expr::StringInterpolation(expressions), + .. + } => { + subexprs = expressions.iter().map(|e| &e.expr).collect(); + } + _ => panic!("Expected an `Expr::StringInterpolation`"), + } + + assert_eq!(subexprs.len(), 2); + + assert_eq!(subexprs[0], &Expr::String("hello ".to_string())); + + assert!(matches!(subexprs[1], &Expr::FullCellPath(..))); + } else { + panic!("Not an expression") + } + } + + #[test] + pub fn parse_string_interpolation_escaped_parenthesis() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"$\"hello \\(39 + 3)\"", true, &[]); + + assert!(err.is_none()); + + assert!(block.len() == 1); + let expressions = &block[0]; + + assert!(expressions.len() == 1); + + if let PipelineElement::Expression(_, expr) = &expressions[0] { + let subexprs: Vec<&Expr>; + match expr { + Expression { + expr: Expr::StringInterpolation(expressions), + .. + } => { + subexprs = expressions.iter().map(|e| &e.expr).collect(); + } + _ => panic!("Expected an `Expr::StringInterpolation`"), + } + + assert_eq!(subexprs.len(), 1); + + assert_eq!(subexprs[0], &Expr::String("hello (39 + 3)".to_string())); + } else { + panic!("Not an expression") + } + } + + #[test] + pub fn parse_string_interpolation_escaped_backslash_before_parenthesis() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse( + &mut working_set, + None, + b"$\"hello \\\\(39 + 3)\"", + true, + &[], + ); + + assert!(err.is_none()); + + assert!(block.len() == 1); + let expressions = &block[0]; + + assert!(expressions.len() == 1); + + if let PipelineElement::Expression(_, expr) = &expressions[0] { + let subexprs: Vec<&Expr>; + match expr { + Expression { + expr: Expr::StringInterpolation(expressions), + .. + } => { + subexprs = expressions.iter().map(|e| &e.expr).collect(); + } + _ => panic!("Expected an `Expr::StringInterpolation`"), + } + + assert_eq!(subexprs.len(), 2); + + assert_eq!(subexprs[0], &Expr::String("hello \\".to_string())); + + assert!(matches!(subexprs[1], &Expr::FullCellPath(..))); + } else { + panic!("Not an expression") + } + } + + #[test] + pub fn parse_string_interpolation_backslash_count_reset_by_expression() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse( + &mut working_set, + None, + b"$\"\\(1 + 3)\\(7 - 5)\"", + true, + &[], + ); + + assert!(err.is_none()); + + assert!(block.len() == 1); + let expressions = &block[0]; + + assert!(expressions.len() == 1); + + if let PipelineElement::Expression(_, expr) = &expressions[0] { + let subexprs: Vec<&Expr>; + match expr { + Expression { + expr: Expr::StringInterpolation(expressions), + .. + } => { + subexprs = expressions.iter().map(|e| &e.expr).collect(); + } + _ => panic!("Expected an `Expr::StringInterpolation`"), + } + + assert_eq!(subexprs.len(), 1); + assert_eq!(subexprs[0], &Expr::String("(1 + 3)(7 - 5)".to_string())); + } else { + panic!("Not an expression") + } + } + + #[test] + pub fn parse_nested_expressions() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_variable( + "foo".to_string().into_bytes(), + Span::new(0, 0), + nu_protocol::Type::CellPath, + false, + ); + + let (_block, err) = parse( + &mut working_set, + None, + br#" + $"(($foo))" + "#, + true, + &[], + ); + + assert!(err.is_none()); + } + + #[test] + pub fn parse_path_expression() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_variable( + "foo".to_string().into_bytes(), + Span::new(0, 0), + nu_protocol::Type::CellPath, + false, + ); + + let (_block, err) = parse( + &mut working_set, + None, + br#" + $"Hello ($foo.bar)" + "#, + true, + &[], + ); + + assert!(err.is_none()); + } + } +} + mod range { use super::*; use nu_protocol::ast::{RangeInclusion, RangeOperator};