Skip to content

Commit

Permalink
atlas: support unicode escapes in expressions (Netflix#1153)
Browse files Browse the repository at this point in the history
Update query parsing to support unicode escapes for special
characters like comma.
  • Loading branch information
brharrington authored Aug 26, 2024
1 parent c0f3e5f commit 638ab3e
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 Netflix, Inc.
* Copyright 2014-2024 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -70,7 +70,7 @@ private static Object parse(String expr) {
String[] parts = expr.split(",");
Deque<Object> stack = new ArrayDeque<>(parts.length);
for (String p : parts) {
String token = p.trim();
String token = unescape(p.trim());
if (token.isEmpty()) {
continue;
}
Expand Down Expand Up @@ -238,4 +238,76 @@ private static void pushIn(Deque<Object> stack, String k, List<String> values) {
else
stack.push(new Query.In(k, new HashSet<>(values)));
}

static boolean isSpecial(int codePoint) {
return codePoint == ',' || Character.isWhitespace(codePoint);
}

static void zeroPad(String str, StringBuilder builder) {
final int width = 4;
final int n = width - str.length();
for (int i = 0; i < n; ++i) {
builder.append('0');
}
builder.append(str);
}

private static void escapeCodePoint(int codePoint, StringBuilder builder) {
builder.append("\\u");
zeroPad(Integer.toHexString(codePoint), builder);
}

/**
* Escape special characters in the input string to unicode escape sequences (uXXXX).
*/
@SuppressWarnings("PMD")
public static String escape(String str) {
final int length = str.length();
StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length;) {
final int cp = str.codePointAt(i);
final int len = Character.charCount(cp);
if (isSpecial(cp))
escapeCodePoint(cp, builder);
else
builder.appendCodePoint(cp);
i += len;
}
return builder.toString();
}

/**
* Unescape unicode characters in the input string. Ignore any invalid or unrecognized
* escape sequences.
*/
@SuppressWarnings("PMD")
public static String unescape(String str) {
final int length = str.length();
StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length; ++i) {
final char c = str.charAt(i);
if (c == '\\') {
// Ensure there is enough space for an encoded character, there must be at
// least 5 characters left in the string (uXXXX).
if (length - i <= 5) {
builder.append(str.substring(i));
i = length;
} else if (str.charAt(i + 1) == 'u') {
try {
int cp = Integer.parseInt(str.substring(i + 2, i + 6), 16);
builder.appendCodePoint(cp);
i += 5;
} catch (NumberFormatException e) {
builder.append(c);
}
} else {
// Some other escape, copy into buffer and move on
builder.append(c);
}
} else {
builder.append(c);
}
}
return builder.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2023 Netflix, Inc.
* Copyright 2014-2024 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,6 +27,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;

/**
* Query for matching based on tags. For more information see
Expand Down Expand Up @@ -497,7 +498,7 @@ final class Has implements KeyQuery {
}

@Override public String toString() {
return k + ",:has";
return Parser.escape(k) + ",:has";
}

@Override public boolean equals(Object obj) {
Expand Down Expand Up @@ -542,7 +543,7 @@ public String value() {
}

@Override public String toString() {
return k + "," + v + ",:eq";
return Parser.escape(k) + "," + Parser.escape(v) + ",:eq";
}

@Override public boolean equals(Object obj) {
Expand Down Expand Up @@ -609,8 +610,11 @@ public Set<String> values() {
}

@Override public String toString() {
String values = String.join(",", vs);
return k + ",(," + values + ",),:in";
StringJoiner joiner = new StringJoiner(",");
for (String v : vs) {
joiner.add(Parser.escape(v));
}
return Parser.escape(k) + ",(," + joiner + ",),:in";
}

@Override public boolean equals(Object obj) {
Expand Down Expand Up @@ -654,7 +658,7 @@ final class LessThan implements KeyQuery {
}

@Override public String toString() {
return k + "," + v + ",:lt";
return Parser.escape(k) + "," + Parser.escape(v) + ",:lt";
}

@Override public boolean equals(Object obj) {
Expand Down Expand Up @@ -694,7 +698,7 @@ final class LessThanEqual implements KeyQuery {
}

@Override public String toString() {
return k + "," + v + ",:le";
return Parser.escape(k) + "," + Parser.escape(v) + ",:le";
}

@Override public boolean equals(Object obj) {
Expand Down Expand Up @@ -734,7 +738,7 @@ final class GreaterThan implements KeyQuery {
}

@Override public String toString() {
return k + "," + v + ",:gt";
return Parser.escape(k) + "," + Parser.escape(v) + ",:gt";
}

@Override public boolean equals(Object obj) {
Expand Down Expand Up @@ -774,7 +778,7 @@ final class GreaterThanEqual implements KeyQuery {
}

@Override public String toString() {
return k + "," + v + ",:ge";
return Parser.escape(k) + "," + Parser.escape(v) + ",:ge";
}

@Override public boolean equals(Object obj) {
Expand Down Expand Up @@ -841,7 +845,7 @@ public boolean alwaysMatches() {
}

@Override public String toString() {
return k + "," + v + "," + name;
return Parser.escape(k) + "," + Parser.escape(v) + "," + name;
}

@Override public boolean equals(Object obj) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2014-2024 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spectator.atlas.impl;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ParserTest {

private static String zeroPad(int i) {
StringBuilder builder = new StringBuilder();
Parser.zeroPad(Integer.toHexString(i), builder);
return builder.toString();
}

@Test
public void escape() {
for (char i = 0; i < Short.MAX_VALUE; ++i) {
String str = Character.toString(i);
String expected = Parser.isSpecial(i) ? "\\u" + zeroPad(i) : str;
Assertions.assertEquals(expected, Parser.escape(str));
}
}

@Test
public void unescape() {
for (char i = 0; i < Short.MAX_VALUE; ++i) {
String str = Character.toString(i);
String escaped = "\\u" + zeroPad(i);
Assertions.assertEquals(str, Parser.unescape(escaped));
}
}

@Test
public void unescapeTooShort() {
String str = "foo\\u000";
Assertions.assertEquals(str, Parser.unescape(str));
}

@Test
public void unescapeUnknownType() {
String str = "foo\\x0000";
Assertions.assertEquals(str, Parser.unescape(str));
}

@Test
public void unescapeInvalid() {
String str = "foo\\uzyff";
Assertions.assertEquals(str, Parser.unescape(str));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 Netflix, Inc.
* Copyright 2014-2024 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -625,4 +625,117 @@ public void simplifyFalse() {
Query q = Parser.parseQuery(":false");
Assertions.assertSame(q, q.simplify(tags("nf.cluster", "foo")));
}

@Test
public void keysAndValuesWithSpecialChars() {
String[] ops = {"eq", "lt", "le", "gt", "ge"};
for (String op : ops) {
String k = "foo\\u002cbar";
String v = "a\\u002cb\\u002cc";
String expr = k + "," + v + ",:" + op;
Query q = Parser.parseQuery(expr);
Assertions.assertEquals(expr, q.toString());

Query.KeyQuery kq = (Query.KeyQuery) q;
Assertions.assertEquals("foo,bar", kq.key());
if ("lt".equals(op)) {
Assertions.assertTrue(kq.matches("a,b,b"));
} else if ("gt".equals(op)) {
Assertions.assertTrue(kq.matches("a,b,d"));
} else {
Assertions.assertTrue(kq.matches("a,b,c"));
}
}
}

@Test
public void inClauseWithSpecialChars() {
String k = "foo\\u002cbar";
String vs = "(,a\\u002cb\\u002cc,d,)";
String expr = k + "," + vs + ",:in";
Query q = Parser.parseQuery(expr);
Assertions.assertEquals(expr, q.toString());

Query.In in = (Query.In) q;
Assertions.assertEquals("foo,bar", in.key());
Assertions.assertTrue(in.matches("a,b,c"));
Assertions.assertTrue(in.matches("d"));
}

@Test
public void hasWithSpecialChars() {
String k = "foo\\u002cbar";
String expr = k + ",:has";
Query q = Parser.parseQuery(expr);
Assertions.assertEquals(expr, q.toString());

Query.Has has = (Query.Has) q;
Assertions.assertEquals("foo,bar", has.key());
}

@Test
public void reWithSpecialChars() {
String k = "foo\\u002cbar";
String v = "a\\u002cb\\u002cc";
String expr = k + "," + v + ",:re";
Query q = Parser.parseQuery(expr);
Assertions.assertEquals(expr, q.toString());

Query.Regex re = (Query.Regex) q;
Assertions.assertEquals("foo,bar", re.key());
Assertions.assertTrue(re.matches("a,b,c"));
}

@Test
public void reicWithSpecialChars() {
String k = "foo\\u002cbar";
String v = "a\\u002cb\\u002cc";
String expr = k + "," + v + ",:reic";
Query q = Parser.parseQuery(expr);
Assertions.assertEquals(expr, q.toString());

Query.Regex re = (Query.Regex) q;
Assertions.assertEquals("foo,bar", re.key());
Assertions.assertTrue(re.matches("a,b,c"));
Assertions.assertTrue(re.matches("a,B,c"));
}

@Test
public void startsWithSpecialChars() {
String k = "foo\\u002cbar";
String v = "a\\u002cb\\u002cc";
String expr = k + "," + v + ",:starts";
Query q = Parser.parseQuery(expr);
Assertions.assertEquals(k + "," + v + ",:re", q.toString());

Query.Regex re = (Query.Regex) q;
Assertions.assertEquals("foo,bar", re.key());
Assertions.assertTrue(re.matches("a,b,c"));
}

@Test
public void endsWithSpecialChars() {
String k = "foo\\u002cbar";
String v = "a\\u002cb\\u002cc";
String expr = k + "," + v + ",:ends";
Query q = Parser.parseQuery(expr);
Assertions.assertEquals(k + ",.*" + v + "$,:re", q.toString());

Query.Regex re = (Query.Regex) q;
Assertions.assertEquals("foo,bar", re.key());
Assertions.assertTrue(re.matches("a,b,c"));
}

@Test
public void containsWithSpecialChars() {
String k = "foo\\u002cbar";
String v = "a\\u002cb\\u002cc";
String expr = k + "," + v + ",:contains";
Query q = Parser.parseQuery(expr);
Assertions.assertEquals(k + ",.*" + v + ",:re", q.toString());

Query.Regex re = (Query.Regex) q;
Assertions.assertEquals("foo,bar", re.key());
Assertions.assertTrue(re.matches("a,b,c"));
}
}

0 comments on commit 638ab3e

Please sign in to comment.