Skip to content

Commit 638ab3e

Browse files
authored
atlas: support unicode escapes in expressions (#1153)
Update query parsing to support unicode escapes for special characters like comma.
1 parent c0f3e5f commit 638ab3e

File tree

4 files changed

+266
-13
lines changed

4 files changed

+266
-13
lines changed

spectator-reg-atlas/src/main/java/com/netflix/spectator/atlas/impl/Parser.java

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 Netflix, Inc.
2+
* Copyright 2014-2024 Netflix, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -70,7 +70,7 @@ private static Object parse(String expr) {
7070
String[] parts = expr.split(",");
7171
Deque<Object> stack = new ArrayDeque<>(parts.length);
7272
for (String p : parts) {
73-
String token = p.trim();
73+
String token = unescape(p.trim());
7474
if (token.isEmpty()) {
7575
continue;
7676
}
@@ -238,4 +238,76 @@ private static void pushIn(Deque<Object> stack, String k, List<String> values) {
238238
else
239239
stack.push(new Query.In(k, new HashSet<>(values)));
240240
}
241+
242+
static boolean isSpecial(int codePoint) {
243+
return codePoint == ',' || Character.isWhitespace(codePoint);
244+
}
245+
246+
static void zeroPad(String str, StringBuilder builder) {
247+
final int width = 4;
248+
final int n = width - str.length();
249+
for (int i = 0; i < n; ++i) {
250+
builder.append('0');
251+
}
252+
builder.append(str);
253+
}
254+
255+
private static void escapeCodePoint(int codePoint, StringBuilder builder) {
256+
builder.append("\\u");
257+
zeroPad(Integer.toHexString(codePoint), builder);
258+
}
259+
260+
/**
261+
* Escape special characters in the input string to unicode escape sequences (uXXXX).
262+
*/
263+
@SuppressWarnings("PMD")
264+
public static String escape(String str) {
265+
final int length = str.length();
266+
StringBuilder builder = new StringBuilder(length);
267+
for (int i = 0; i < length;) {
268+
final int cp = str.codePointAt(i);
269+
final int len = Character.charCount(cp);
270+
if (isSpecial(cp))
271+
escapeCodePoint(cp, builder);
272+
else
273+
builder.appendCodePoint(cp);
274+
i += len;
275+
}
276+
return builder.toString();
277+
}
278+
279+
/**
280+
* Unescape unicode characters in the input string. Ignore any invalid or unrecognized
281+
* escape sequences.
282+
*/
283+
@SuppressWarnings("PMD")
284+
public static String unescape(String str) {
285+
final int length = str.length();
286+
StringBuilder builder = new StringBuilder(length);
287+
for (int i = 0; i < length; ++i) {
288+
final char c = str.charAt(i);
289+
if (c == '\\') {
290+
// Ensure there is enough space for an encoded character, there must be at
291+
// least 5 characters left in the string (uXXXX).
292+
if (length - i <= 5) {
293+
builder.append(str.substring(i));
294+
i = length;
295+
} else if (str.charAt(i + 1) == 'u') {
296+
try {
297+
int cp = Integer.parseInt(str.substring(i + 2, i + 6), 16);
298+
builder.appendCodePoint(cp);
299+
i += 5;
300+
} catch (NumberFormatException e) {
301+
builder.append(c);
302+
}
303+
} else {
304+
// Some other escape, copy into buffer and move on
305+
builder.append(c);
306+
}
307+
} else {
308+
builder.append(c);
309+
}
310+
}
311+
return builder.toString();
312+
}
241313
}

spectator-reg-atlas/src/main/java/com/netflix/spectator/atlas/impl/Query.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2023 Netflix, Inc.
2+
* Copyright 2014-2024 Netflix, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
import java.util.Map;
2828
import java.util.Objects;
2929
import java.util.Set;
30+
import java.util.StringJoiner;
3031

3132
/**
3233
* Query for matching based on tags. For more information see
@@ -497,7 +498,7 @@ final class Has implements KeyQuery {
497498
}
498499

499500
@Override public String toString() {
500-
return k + ",:has";
501+
return Parser.escape(k) + ",:has";
501502
}
502503

503504
@Override public boolean equals(Object obj) {
@@ -542,7 +543,7 @@ public String value() {
542543
}
543544

544545
@Override public String toString() {
545-
return k + "," + v + ",:eq";
546+
return Parser.escape(k) + "," + Parser.escape(v) + ",:eq";
546547
}
547548

548549
@Override public boolean equals(Object obj) {
@@ -609,8 +610,11 @@ public Set<String> values() {
609610
}
610611

611612
@Override public String toString() {
612-
String values = String.join(",", vs);
613-
return k + ",(," + values + ",),:in";
613+
StringJoiner joiner = new StringJoiner(",");
614+
for (String v : vs) {
615+
joiner.add(Parser.escape(v));
616+
}
617+
return Parser.escape(k) + ",(," + joiner + ",),:in";
614618
}
615619

616620
@Override public boolean equals(Object obj) {
@@ -654,7 +658,7 @@ final class LessThan implements KeyQuery {
654658
}
655659

656660
@Override public String toString() {
657-
return k + "," + v + ",:lt";
661+
return Parser.escape(k) + "," + Parser.escape(v) + ",:lt";
658662
}
659663

660664
@Override public boolean equals(Object obj) {
@@ -694,7 +698,7 @@ final class LessThanEqual implements KeyQuery {
694698
}
695699

696700
@Override public String toString() {
697-
return k + "," + v + ",:le";
701+
return Parser.escape(k) + "," + Parser.escape(v) + ",:le";
698702
}
699703

700704
@Override public boolean equals(Object obj) {
@@ -734,7 +738,7 @@ final class GreaterThan implements KeyQuery {
734738
}
735739

736740
@Override public String toString() {
737-
return k + "," + v + ",:gt";
741+
return Parser.escape(k) + "," + Parser.escape(v) + ",:gt";
738742
}
739743

740744
@Override public boolean equals(Object obj) {
@@ -774,7 +778,7 @@ final class GreaterThanEqual implements KeyQuery {
774778
}
775779

776780
@Override public String toString() {
777-
return k + "," + v + ",:ge";
781+
return Parser.escape(k) + "," + Parser.escape(v) + ",:ge";
778782
}
779783

780784
@Override public boolean equals(Object obj) {
@@ -841,7 +845,7 @@ public boolean alwaysMatches() {
841845
}
842846

843847
@Override public String toString() {
844-
return k + "," + v + "," + name;
848+
return Parser.escape(k) + "," + Parser.escape(v) + "," + name;
845849
}
846850

847851
@Override public boolean equals(Object obj) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2014-2024 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spectator.atlas.impl;
17+
18+
import org.junit.jupiter.api.Assertions;
19+
import org.junit.jupiter.api.Test;
20+
21+
public class ParserTest {
22+
23+
private static String zeroPad(int i) {
24+
StringBuilder builder = new StringBuilder();
25+
Parser.zeroPad(Integer.toHexString(i), builder);
26+
return builder.toString();
27+
}
28+
29+
@Test
30+
public void escape() {
31+
for (char i = 0; i < Short.MAX_VALUE; ++i) {
32+
String str = Character.toString(i);
33+
String expected = Parser.isSpecial(i) ? "\\u" + zeroPad(i) : str;
34+
Assertions.assertEquals(expected, Parser.escape(str));
35+
}
36+
}
37+
38+
@Test
39+
public void unescape() {
40+
for (char i = 0; i < Short.MAX_VALUE; ++i) {
41+
String str = Character.toString(i);
42+
String escaped = "\\u" + zeroPad(i);
43+
Assertions.assertEquals(str, Parser.unescape(escaped));
44+
}
45+
}
46+
47+
@Test
48+
public void unescapeTooShort() {
49+
String str = "foo\\u000";
50+
Assertions.assertEquals(str, Parser.unescape(str));
51+
}
52+
53+
@Test
54+
public void unescapeUnknownType() {
55+
String str = "foo\\x0000";
56+
Assertions.assertEquals(str, Parser.unescape(str));
57+
}
58+
59+
@Test
60+
public void unescapeInvalid() {
61+
String str = "foo\\uzyff";
62+
Assertions.assertEquals(str, Parser.unescape(str));
63+
}
64+
}

spectator-reg-atlas/src/test/java/com/netflix/spectator/atlas/impl/QueryTest.java

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2021 Netflix, Inc.
2+
* Copyright 2014-2024 Netflix, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -625,4 +625,117 @@ public void simplifyFalse() {
625625
Query q = Parser.parseQuery(":false");
626626
Assertions.assertSame(q, q.simplify(tags("nf.cluster", "foo")));
627627
}
628+
629+
@Test
630+
public void keysAndValuesWithSpecialChars() {
631+
String[] ops = {"eq", "lt", "le", "gt", "ge"};
632+
for (String op : ops) {
633+
String k = "foo\\u002cbar";
634+
String v = "a\\u002cb\\u002cc";
635+
String expr = k + "," + v + ",:" + op;
636+
Query q = Parser.parseQuery(expr);
637+
Assertions.assertEquals(expr, q.toString());
638+
639+
Query.KeyQuery kq = (Query.KeyQuery) q;
640+
Assertions.assertEquals("foo,bar", kq.key());
641+
if ("lt".equals(op)) {
642+
Assertions.assertTrue(kq.matches("a,b,b"));
643+
} else if ("gt".equals(op)) {
644+
Assertions.assertTrue(kq.matches("a,b,d"));
645+
} else {
646+
Assertions.assertTrue(kq.matches("a,b,c"));
647+
}
648+
}
649+
}
650+
651+
@Test
652+
public void inClauseWithSpecialChars() {
653+
String k = "foo\\u002cbar";
654+
String vs = "(,a\\u002cb\\u002cc,d,)";
655+
String expr = k + "," + vs + ",:in";
656+
Query q = Parser.parseQuery(expr);
657+
Assertions.assertEquals(expr, q.toString());
658+
659+
Query.In in = (Query.In) q;
660+
Assertions.assertEquals("foo,bar", in.key());
661+
Assertions.assertTrue(in.matches("a,b,c"));
662+
Assertions.assertTrue(in.matches("d"));
663+
}
664+
665+
@Test
666+
public void hasWithSpecialChars() {
667+
String k = "foo\\u002cbar";
668+
String expr = k + ",:has";
669+
Query q = Parser.parseQuery(expr);
670+
Assertions.assertEquals(expr, q.toString());
671+
672+
Query.Has has = (Query.Has) q;
673+
Assertions.assertEquals("foo,bar", has.key());
674+
}
675+
676+
@Test
677+
public void reWithSpecialChars() {
678+
String k = "foo\\u002cbar";
679+
String v = "a\\u002cb\\u002cc";
680+
String expr = k + "," + v + ",:re";
681+
Query q = Parser.parseQuery(expr);
682+
Assertions.assertEquals(expr, q.toString());
683+
684+
Query.Regex re = (Query.Regex) q;
685+
Assertions.assertEquals("foo,bar", re.key());
686+
Assertions.assertTrue(re.matches("a,b,c"));
687+
}
688+
689+
@Test
690+
public void reicWithSpecialChars() {
691+
String k = "foo\\u002cbar";
692+
String v = "a\\u002cb\\u002cc";
693+
String expr = k + "," + v + ",:reic";
694+
Query q = Parser.parseQuery(expr);
695+
Assertions.assertEquals(expr, q.toString());
696+
697+
Query.Regex re = (Query.Regex) q;
698+
Assertions.assertEquals("foo,bar", re.key());
699+
Assertions.assertTrue(re.matches("a,b,c"));
700+
Assertions.assertTrue(re.matches("a,B,c"));
701+
}
702+
703+
@Test
704+
public void startsWithSpecialChars() {
705+
String k = "foo\\u002cbar";
706+
String v = "a\\u002cb\\u002cc";
707+
String expr = k + "," + v + ",:starts";
708+
Query q = Parser.parseQuery(expr);
709+
Assertions.assertEquals(k + "," + v + ",:re", q.toString());
710+
711+
Query.Regex re = (Query.Regex) q;
712+
Assertions.assertEquals("foo,bar", re.key());
713+
Assertions.assertTrue(re.matches("a,b,c"));
714+
}
715+
716+
@Test
717+
public void endsWithSpecialChars() {
718+
String k = "foo\\u002cbar";
719+
String v = "a\\u002cb\\u002cc";
720+
String expr = k + "," + v + ",:ends";
721+
Query q = Parser.parseQuery(expr);
722+
Assertions.assertEquals(k + ",.*" + v + "$,:re", q.toString());
723+
724+
Query.Regex re = (Query.Regex) q;
725+
Assertions.assertEquals("foo,bar", re.key());
726+
Assertions.assertTrue(re.matches("a,b,c"));
727+
}
728+
729+
@Test
730+
public void containsWithSpecialChars() {
731+
String k = "foo\\u002cbar";
732+
String v = "a\\u002cb\\u002cc";
733+
String expr = k + "," + v + ",:contains";
734+
Query q = Parser.parseQuery(expr);
735+
Assertions.assertEquals(k + ",.*" + v + ",:re", q.toString());
736+
737+
Query.Regex re = (Query.Regex) q;
738+
Assertions.assertEquals("foo,bar", re.key());
739+
Assertions.assertTrue(re.matches("a,b,c"));
740+
}
628741
}

0 commit comments

Comments
 (0)