Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/label/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use regex::Regex;

use crate::parser::lex::is_label;
use crate::parser::token::{token_display, TokenId, T_EQL, T_EQL_REGEX, T_NEQ, T_NEQ_REGEX};
use crate::util::join_vector;
use crate::util::{escape_string, join_vector};

const LABEL_METRIC_NAME: &str = "__name__";

Expand Down Expand Up @@ -152,9 +152,9 @@ impl fmt::Display for Matcher {
let name = if is_label(&self.name) {
self.name.clone()
} else {
format!("\"{}\"", self.name)
format!("\"{}\"", escape_string(&self.name))
};
write!(f, "{}{}\"{}\"", name, self.op, self.value)
write!(f, "{}{}\"{}\"", name, self.op, escape_string(&self.value))
Comment thread
harry671003 marked this conversation as resolved.
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/label/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::collections::HashSet;
use std::fmt;

use crate::parser::lex::is_label;
use crate::util::escape_string;

mod matcher;
pub use matcher::{MatchOp, Matcher, Matchers};
Expand Down Expand Up @@ -78,7 +79,7 @@ impl fmt::Display for Labels {
if is_label(label) {
label.clone()
} else {
format!("\"{}\"", label)
format!("\"{}\"", escape_string(label))
}
})
.collect();
Expand Down
42 changes: 40 additions & 2 deletions src/parser/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::parser::token::{
use crate::parser::token::{Token, TokenId, TokenType};
use crate::parser::value::ValueType;
use crate::parser::{indent, Function, FunctionArgs, Prettier, MAX_CHARACTERS_PER_LINE};
use crate::util::display_duration;
use crate::util::{display_duration, escape_string};
use chrono::{DateTime, Utc};
use std::fmt::{self, Write};
use std::ops::Neg;
Expand Down Expand Up @@ -870,7 +870,7 @@ pub struct StringLiteral {

impl fmt::Display for StringLiteral {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\"{}\"", self.val)
write!(f, "\"{}\"", escape_string(&self.val))
}
}

Expand Down Expand Up @@ -2973,4 +2973,42 @@ or
assert_eq!(prettified, expected);
}
}

#[test]
fn test_prettify_escape_roundtrip() {
// Queries with backslash escapes must survive parse → prettify → re-parse
let cases = vec![
// Escaped dot in regex matcher
(
r#"{__name__="up",service=~"flagd\\.evaluation\\.v1\\.Service"}"#,
r#"{__name__="up",service=~"flagd\\.evaluation\\.v1\\.Service"}"#,
),
// Escaped pipe
(
r#"{__name__="up",tag=~"a\\|b"}"#,
r#"{__name__="up",tag=~"a\\|b"}"#,
),
// Literal backslash in value
(r#"{path="C:\\\\Windows"}"#, r#"{path="C:\\\\Windows"}"#),
// Embedded double quote
(r#"{msg="say \"hello\""}"#, r#"{msg="say \"hello\""}"#),
];

for (input, expected) in &cases {
let parsed = crate::parser::parse(input).unwrap();
let prettified = parsed.prettify();
assert_eq!(
&prettified, expected,
"prettify mismatch for input: {input}"
);

// Roundtrip: re-parsing the prettified output must succeed and produce the same result
let reparsed = crate::parser::parse(&prettified).unwrap();
assert_eq!(
parsed.prettify(),
reparsed.prettify(),
"roundtrip failed for input: {input}"
);
}
}
}
2 changes: 1 addition & 1 deletion src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub mod visitor;

pub use duration::{display_duration, parse_duration};
pub use number::parse_str_radix;
pub use string::unquote_string;
pub use string::{escape_string, unquote_string};
pub use visitor::{walk_expr, ExprVisitor};

pub(crate) fn join_vector<T: std::fmt::Display>(v: &[T], sep: &str, sort: bool) -> String {
Expand Down
45 changes: 45 additions & 0 deletions src/util/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@

//! Internal utilities for strings.

/// Escapes a string value for embedding in a PromQL double-quoted string literal.
/// This is the inverse of `unquote_string` — it re-escapes backslashes and double quotes.
pub fn escape_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => result.push_str("\\\\"),
'"' => result.push_str("\\\""),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ => result.push(c),
}
}
result
}

/// This function is modified from original go version
/// https://github.com/prometheus/prometheus/blob/v3.8.0/util/strutil/quote.go
pub fn unquote_string(s: &str) -> Result<String, String> {
Expand Down Expand Up @@ -346,4 +363,32 @@ mod tests {
assert!(unquote_string("`hello`world`").is_err());
assert!(unquote_string("``hello`").is_err());
}

#[test]
fn test_escape_string() {
assert_eq!(escape_string("hello"), "hello");
assert_eq!(escape_string(r#"say "hi""#), r#"say \"hi\""#);
assert_eq!(escape_string("back\\slash"), "back\\\\slash");
assert_eq!(escape_string("new\nline"), "new\\nline");
assert_eq!(escape_string("tab\there"), "tab\\there");
assert_eq!(escape_string("cr\rhere"), "cr\\rhere");
}

#[test]
fn test_escape_unquote_roundtrip() {
// escape_string should produce output that unquote_string can reverse
let values = vec![
"hello",
"flagd\\.eval",
"a\\|b",
"C:\\\\Windows",
"say \"hi\"",
];
for val in values {
let escaped = escape_string(val);
let quoted = format!("\"{}\"", escaped);
let unquoted = unquote_string(&quoted).unwrap();
assert_eq!(unquoted, val, "roundtrip failed for: {val:?}");
}
}
}
Loading