From e0c86f16e88b1ca71672b938d15b21b99ee911f9 Mon Sep 17 00:00:00 2001 From: martinohmann Date: Fri, 16 Jun 2023 20:26:07 +0200 Subject: [PATCH] fix(string): properly handle escaping of interpolation/directive marker (#249) Closes #248 This is the same fix as made in #247 for template literals, just for strings. --- crates/hcl-edit/src/encode/mod.rs | 4 +++- crates/hcl-edit/src/parser/string.rs | 3 ++- crates/hcl-edit/tests/regressions.rs | 13 +++++++++++++ crates/hcl-rs/src/format/impls.rs | 14 ++++++++++---- crates/hcl-rs/src/format/mod.rs | 20 ++++++++++++-------- crates/hcl-rs/src/parser/mod.rs | 3 ++- crates/hcl-rs/tests/regressions.rs | 16 +++++++++++++++- 7 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 crates/hcl-edit/tests/regressions.rs diff --git a/crates/hcl-edit/src/encode/mod.rs b/crates/hcl-edit/src/encode/mod.rs index 92a378ee..b8391c3a 100644 --- a/crates/hcl-edit/src/encode/mod.rs +++ b/crates/hcl-edit/src/encode/mod.rs @@ -3,6 +3,7 @@ mod structure; mod template; use crate::{Decorate, Decorated, Formatted, Ident, Number}; +use hcl_primitives::template::escape_markers; use std::fmt::{self, Write}; pub(crate) const NO_DECOR: (&str, &str) = ("", ""); @@ -122,7 +123,8 @@ where fn encode_quoted_string(buf: &mut dyn fmt::Write, value: &str) -> fmt::Result { buf.write_char('"')?; - encode_escaped(buf, value)?; + let value = escape_markers(value); + encode_escaped(buf, &value)?; buf.write_char('"') } diff --git a/crates/hcl-edit/src/parser/string.rs b/crates/hcl-edit/src/parser/string.rs index 9fd049f0..6d1c821f 100644 --- a/crates/hcl-edit/src/parser/string.rs +++ b/crates/hcl-edit/src/parser/string.rs @@ -5,6 +5,7 @@ use super::{ IResult, Input, }; use crate::{Decorated, Ident, RawString}; +use hcl_primitives::template::unescape_markers; use std::borrow::Cow; use winnow::{ combinator::{alt, cut_err, delimited, fail, not, opt, preceded, repeat, success}, @@ -17,7 +18,7 @@ use winnow::{ pub(super) fn string(input: Input) -> IResult { delimited(b'"', opt(build_string), b'"') .map(Option::unwrap_or_default) - .output_into() + .map(|s| unescape_markers(&s).into()) .parse_next(input) } diff --git a/crates/hcl-edit/tests/regressions.rs b/crates/hcl-edit/tests/regressions.rs new file mode 100644 index 00000000..c02bdd59 --- /dev/null +++ b/crates/hcl-edit/tests/regressions.rs @@ -0,0 +1,13 @@ +use hcl_edit::expr::Expression; + +// https://github.com/martinohmann/hcl-rs/issues/248 +#[test] +fn issue_248() { + let expr = Expression::from("${foo}"); + + let encoded = expr.to_string(); + assert_eq!(encoded, "\"$${foo}\""); + + let parsed: Expression = encoded.parse().unwrap(); + assert_eq!(parsed, expr); +} diff --git a/crates/hcl-rs/src/format/impls.rs b/crates/hcl-rs/src/format/impls.rs index 55d26103..9421b2de 100644 --- a/crates/hcl-rs/src/format/impls.rs +++ b/crates/hcl-rs/src/format/impls.rs @@ -148,7 +148,13 @@ impl Format for Value { Value::Null => Ok(fmt.write_null()?), Value::Bool(b) => Ok(fmt.write_bool(*b)?), Value::Number(num) => num.format(fmt), - Value::String(string) => string.format(fmt), + Value::String(string) => { + if is_templated(string) { + fmt.write_quoted_string(string) + } else { + fmt.write_quoted_string_escaped(string) + } + } Value::Array(array) => format_array(fmt, array.iter()), Value::Object(object) => format_object(fmt, object.iter().map(|(k, v)| (StrKey(k), v))), } @@ -193,7 +199,7 @@ impl<'a> Format for StrKey<'a> { if fmt.config.prefer_ident_keys && is_ident(self.0) { fmt.write_string_fragment(self.0) } else { - fmt.write_quoted_string(self.0, !is_templated(self.0)) + fmt.write_quoted_string_escaped(self.0) } } } @@ -217,7 +223,7 @@ impl Format for TemplateExpr { W: io::Write, { match self { - TemplateExpr::QuotedString(string) => string.format(fmt), + TemplateExpr::QuotedString(string) => fmt.write_quoted_string(string), TemplateExpr::Heredoc(heredoc) => heredoc.format(fmt), } } @@ -556,7 +562,7 @@ impl Format for String { where W: io::Write, { - fmt.write_quoted_string(self, !is_templated(self)) + fmt.write_quoted_string_escaped(self) } } diff --git a/crates/hcl-rs/src/format/mod.rs b/crates/hcl-rs/src/format/mod.rs index ef80bef8..35c43da0 100644 --- a/crates/hcl-rs/src/format/mod.rs +++ b/crates/hcl-rs/src/format/mod.rs @@ -39,6 +39,7 @@ mod impls; use self::escape::{CharEscape, ESCAPE}; use crate::Result; +use hcl_primitives::template::escape_markers; use std::io; mod private { @@ -425,15 +426,17 @@ where self.write_bytes(s.as_bytes()) } - /// Writes a quoted string to the writer. The quoted string will be escaped if `escape` is - /// true. - fn write_quoted_string(&mut self, s: &str, escape: bool) -> Result<()> { + /// Writes a quoted string to the writer. + fn write_quoted_string(&mut self, s: &str) -> Result<()> { self.write_bytes(b"\"")?; - if escape { - self.write_escaped_string(s)?; - } else { - self.write_string_fragment(s)?; - } + self.write_string_fragment(s)?; + self.write_bytes(b"\"") + } + + /// Writes a quoted string to the writer after escaping it. + fn write_quoted_string_escaped(&mut self, s: &str) -> Result<()> { + self.write_bytes(b"\"")?; + self.write_escaped_string(s)?; self.write_bytes(b"\"") } @@ -445,6 +448,7 @@ where /// Writes a string to the writer and escapes control characters and quotes that might be /// contained in it. fn write_escaped_string(&mut self, value: &str) -> Result<()> { + let value = escape_markers(value); let bytes = value.as_bytes(); let mut start = 0; diff --git a/crates/hcl-rs/src/parser/mod.rs b/crates/hcl-rs/src/parser/mod.rs index cf12f131..e95bde10 100644 --- a/crates/hcl-rs/src/parser/mod.rs +++ b/crates/hcl-rs/src/parser/mod.rs @@ -9,6 +9,7 @@ use crate::{ expr::Expression, structure::Body, template::Template, util::unescape, Identifier, Number, Result, }; +use hcl_primitives::template::unescape_markers; use pest::{ iterators::{Pair, Pairs}, Parser as _, @@ -76,7 +77,7 @@ fn string(pair: Pair) -> String { } fn unescape_string(pair: Pair) -> Result { - unescape(pair.as_str()).map(|c| c.to_string()) + unescape(pair.as_str()).map(|c| unescape_markers(&c).to_string()) } fn ident(pair: Pair) -> Identifier { diff --git a/crates/hcl-rs/tests/regressions.rs b/crates/hcl-rs/tests/regressions.rs index 59ae1340..1e15fd09 100644 --- a/crates/hcl-rs/tests/regressions.rs +++ b/crates/hcl-rs/tests/regressions.rs @@ -2,7 +2,7 @@ mod common; use common::{assert_deserialize, assert_format}; use hcl::eval::{Context, Evaluate}; -use hcl::{expr::*, Body, Identifier, Value}; +use hcl::{expr::*, Attribute, Body, Identifier, Value}; use indoc::indoc; use serde::Deserialize; use std::collections::HashMap; @@ -225,3 +225,17 @@ fn issue_242() { assert_eq!(value, Value::from("make TARGET=${GIT_BRANCH}\n")); } + +// https://github.com/martinohmann/hcl-rs/issues/248 +#[test] +fn issue_248() { + let body = Body::builder() + .add_attribute(Attribute::new("attr", "${foo}")) + .build(); + + let formatted = hcl::format::to_string(&body).unwrap(); + assert_eq!(formatted, "attr = \"$${foo}\"\n"); + + let parsed = hcl::parse(&formatted).unwrap(); + assert_eq!(parsed, body); +}