Skip to content

Commit

Permalink
fix(template): properly handle escaping of interpolation/directive ma…
Browse files Browse the repository at this point in the history
…rkers

Fixes #242
Closes #243

This fixes the handling of `${` and `%{` markers in template literals.

When a HCL template literal contains escaped interpolation sequence or
directive control flow start markers (`$${` and `%%{` respectively),
they will be automatically unescaped while parsing now.

Additionally, formatting a `Element::Literal` containing `${` or `%{` as
HCL will now correctly emit `$${` and `%%{`.
  • Loading branch information
martinohmann committed Jun 15, 2023
1 parent 5e51771 commit dcde671
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 10 deletions.
6 changes: 4 additions & 2 deletions crates/hcl-edit/src/encode/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::template::{
Strip, Template,
};
use crate::util::indent_by;
use hcl_primitives::template::escape_markers;
use std::fmt::{self, Write};

const INTERPOLATION_START: &str = "${";
Expand Down Expand Up @@ -68,10 +69,11 @@ impl Encode for Element {
fn encode(&self, buf: &mut EncodeState) -> fmt::Result {
match self {
Element::Literal(lit) => {
let escaped = escape_markers(lit);
if buf.escape() {
encode_escaped(buf, lit)
encode_escaped(buf, &escaped)
} else {
buf.write_str(lit)
buf.write_str(&escaped)
}
}
Element::Interpolation(interp) => interp.encode(buf),
Expand Down
5 changes: 3 additions & 2 deletions crates/hcl-edit/src/parser/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ use winnow::{
pub(super) fn string(input: Input) -> IResult<Input, String> {
delimited(b'"', opt(build_string), b'"')
.map(Option::unwrap_or_default)
.output_into()
.parse_next(input)
}

pub(super) fn build_string(input: Input) -> IResult<Input, String> {
pub(super) fn build_string(input: Input) -> IResult<Input, Cow<str>> {
let (mut input, mut string) = match string_fragment(input) {
Ok((input, fragment)) => match fragment {
StringFragment::Literal(s) => (input, Cow::Borrowed(s)),
Expand All @@ -38,7 +39,7 @@ pub(super) fn build_string(input: Input) -> IResult<Input, String> {
};
input = rest;
}
Err(_) => return Ok((input, string.into())),
Err(_) => return Ok((input, string)),
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions crates/hcl-edit/src/parser/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ use crate::{
},
SetSpan, Span, Spanned,
};
use hcl_primitives::template::unescape_markers;
use std::borrow::Cow;
use winnow::{
ascii::{line_ending, space0},
combinator::{alt, delimited, opt, preceded, repeat, separated_pair, terminated},
Expand Down Expand Up @@ -79,12 +81,12 @@ pub(super) fn heredoc_template<'a>(

fn elements<'a, P>(literal: P) -> impl Parser<Input<'a>, Vec<Element>, ParseError<Input<'a>>>
where
P: Parser<Input<'a>, String, ParseError<Input<'a>>>,
P: Parser<Input<'a>, Cow<'a, str>, ParseError<Input<'a>>>,
{
repeat(
0..,
spanned(alt((
literal.map(|s| Element::Literal(Spanned::new(s))),
literal.map(|s| Element::Literal(Spanned::new(unescape_markers(&s).into()))),
interpolation.map(Element::Interpolation),
directive.map(Element::Directive),
))),
Expand Down
18 changes: 17 additions & 1 deletion crates/hcl-edit/src/parser/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ use super::expr::expr;
use super::parse_complete;
use super::structure::body;
use super::template::template;
use crate::{expr::Expression, Formatted, Number};
use crate::{
expr::Expression,
template::{Element, Interpolation, StringTemplate},
Formatted, Ident, Number,
};
use indoc::indoc;
use pretty_assertions::assert_eq;

Expand All @@ -21,6 +25,17 @@ fn number_expr() {
assert_eq!(parsed, expected);
}

#[test]
fn escaped_string_template() {
let parsed = parse_complete(r#""$${escaped} ${unescaped}""#, expr).unwrap();
let template = StringTemplate::from_iter([
Element::Literal(String::from("${escaped} ").into()),
Element::Interpolation(Interpolation::new(Ident::new("unescaped"))),
]);
let expected = Expression::Template(template);
assert_eq!(parsed, expected);
}

#[test]
fn roundtrip_expr() {
let inputs = [
Expand Down Expand Up @@ -120,6 +135,7 @@ fn roundtrip_template() {
- ${item}
%{ endfor ~}
"#},
"literal $${escaped} ${value}",
];

for input in inputs {
Expand Down
19 changes: 19 additions & 0 deletions crates/hcl-rs/src/eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@
//! # }
//! ```
//!
//! If you need to include the literal representation of variable reference, you can escape `${`
//! with `$${`:
//!
//! ```
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use hcl::eval::{Context, Evaluate};
//! use hcl::Template;
//! use std::str::FromStr;
//!
//! let template = Template::from_str("Value: ${value}, escaped: $${value}")?;
//! let mut ctx = Context::new();
//! ctx.declare_var("value", 1);
//!
//! let evaluated = "Value: 1, escaped: ${value}";
//! assert_eq!(template.evaluate(&ctx)?, evaluated);
//! # Ok(())
//! # }
//! ```
//!
//! Here's another example which evaluates some attribute expressions using [`from_str`] as
//! described in the [deserialization
//! example][crate::eval#expression-evaluation-during-de-serialization] below:
Expand Down
6 changes: 5 additions & 1 deletion crates/hcl-rs/src/format/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::template::{
use crate::util::is_templated;
use crate::{Identifier, Number, Result, Value};
use hcl_primitives::ident::is_ident;
use hcl_primitives::template::escape_markers;
use std::io;

impl<T> private::Sealed for &T where T: Format {}
Expand Down Expand Up @@ -466,7 +467,10 @@ impl Format for Element {
W: io::Write,
{
match self {
Element::Literal(lit) => fmt.write_string_fragment(lit),
Element::Literal(lit) => {
let escaped = escape_markers(lit);
fmt.write_string_fragment(&escaped)
}
Element::Interpolation(interp) => interp.format(fmt),
Element::Directive(dir) => dir.format(fmt),
}
Expand Down
3 changes: 2 additions & 1 deletion crates/hcl-rs/src/parser/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ use super::*;
use crate::template::{
Directive, Element, ForDirective, IfDirective, Interpolation, Strip, Template,
};
use hcl_primitives::template::unescape_markers;

pub fn template(pair: Pair<Rule>) -> Result<Template> {
pair.into_inner().map(element).collect()
}

fn element(pair: Pair<Rule>) -> Result<Element> {
match pair.as_rule() {
Rule::TemplateLiteral => Ok(Element::Literal(string(pair))),
Rule::TemplateLiteral => Ok(Element::Literal(unescape_markers(pair.as_str()).into())),
Rule::TemplateInterpolation => interpolation(pair).map(Element::Interpolation),
Rule::TemplateDirective => directive(inner(pair)).map(Element::Directive),
rule => unexpected_rule(rule),
Expand Down
26 changes: 25 additions & 1 deletion crates/hcl-rs/tests/regressions.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
mod common;

use common::{assert_deserialize, assert_format};
use hcl::{expr::*, Identifier};
use hcl::eval::{Context, Evaluate};
use hcl::{expr::*, Body, Identifier, Value};
use indoc::indoc;
use serde::Deserialize;
use std::collections::HashMap;
Expand Down Expand Up @@ -201,3 +202,26 @@ fn issue_131() {
.trim_end(),
);
}

// https://github.com/martinohmann/hcl-rs/issues/242
#[test]
fn issue_242() {
let body: Body = hcl::from_str(indoc! {r#"
b = <<-EOF
make TARGET=$${GIT_BRANCH}
EOF
"#
})
.unwrap();

let context = Context::new();
let value = body
.into_attributes()
.find(|attr| attr.key == Identifier::from("b"))
.expect("key not found")
.expr
.evaluate(&context)
.unwrap();

assert_eq!(value, Value::from("make TARGET=${GIT_BRANCH}\n"));
}

0 comments on commit dcde671

Please sign in to comment.