Skip to content

Commit

Permalink
fix(string): properly handle escaping of interpolation/directive mark…
Browse files Browse the repository at this point in the history
…er (#249)

Closes #248

This is the same fix as made in #247 for template literals, just for
strings.
  • Loading branch information
martinohmann committed Jun 16, 2023
1 parent 4040fd2 commit e0c86f1
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 16 deletions.
4 changes: 3 additions & 1 deletion crates/hcl-edit/src/encode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) = ("", "");
Expand Down Expand Up @@ -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('"')
}

Expand Down
3 changes: 2 additions & 1 deletion crates/hcl-edit/src/parser/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -17,7 +18,7 @@ use winnow::{
pub(super) fn string(input: Input) -> IResult<Input, String> {
delimited(b'"', opt(build_string), b'"')
.map(Option::unwrap_or_default)
.output_into()
.map(|s| unescape_markers(&s).into())
.parse_next(input)
}

Expand Down
13 changes: 13 additions & 0 deletions crates/hcl-edit/tests/regressions.rs
Original file line number Diff line number Diff line change
@@ -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);
}
14 changes: 10 additions & 4 deletions crates/hcl-rs/src/format/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
}
Expand Down Expand Up @@ -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)
}
}
}
Expand All @@ -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),
}
}
Expand Down Expand Up @@ -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)
}
}

Expand Down
20 changes: 12 additions & 8 deletions crates/hcl-rs/src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"\"")
}

Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion crates/hcl-rs/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _,
Expand Down Expand Up @@ -76,7 +77,7 @@ fn string(pair: Pair<Rule>) -> String {
}

fn unescape_string(pair: Pair<Rule>) -> Result<String> {
unescape(pair.as_str()).map(|c| c.to_string())
unescape(pair.as_str()).map(|c| unescape_markers(&c).to_string())
}

fn ident(pair: Pair<Rule>) -> Identifier {
Expand Down
16 changes: 15 additions & 1 deletion crates/hcl-rs/tests/regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

0 comments on commit e0c86f1

Please sign in to comment.