Skip to content

Commit fa6bfa1

Browse files
l46kokcopybara-github
authored andcommitted
Add encoders extension library with base64 encoding/decoding capabilities
PiperOrigin-RevId: 552931694
1 parent df15385 commit fa6bfa1

File tree

6 files changed

+285
-16
lines changed

6 files changed

+285
-16
lines changed

extensions/src/main/java/dev/cel/extensions/BUILD.bazel

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ java_library(
1515
],
1616
deps = [
1717
":bindings",
18+
":encoders",
1819
":math",
1920
":protos",
2021
":strings",
@@ -90,6 +91,20 @@ java_library(
9091
],
9192
)
9293

94+
java_library(
95+
name = "encoders",
96+
srcs = ["CelEncoderExtensions.java"],
97+
deps = [
98+
"//checker:checker_builder",
99+
"//common:compiler_common",
100+
"//common/types",
101+
"//compiler:compiler_builder",
102+
"//runtime",
103+
"@maven//:com_google_errorprone_error_prone_annotations",
104+
"@maven//:com_google_protobuf_protobuf_java",
105+
],
106+
)
107+
93108
java_library(
94109
name = "optional_library",
95110
srcs = ["CelOptionalLibrary.java"],
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.extensions;
16+
17+
import com.google.errorprone.annotations.Immutable;
18+
import com.google.protobuf.ByteString;
19+
import dev.cel.checker.CelCheckerBuilder;
20+
import dev.cel.common.CelFunctionDecl;
21+
import dev.cel.common.CelOverloadDecl;
22+
import dev.cel.common.types.SimpleType;
23+
import dev.cel.compiler.CelCompilerLibrary;
24+
import dev.cel.runtime.CelRuntime;
25+
import dev.cel.runtime.CelRuntimeBuilder;
26+
import dev.cel.runtime.CelRuntimeLibrary;
27+
import java.util.Base64;
28+
import java.util.Base64.Decoder;
29+
import java.util.Base64.Encoder;
30+
31+
/** Internal implementation of Encoder Extensions. */
32+
@Immutable
33+
public class CelEncoderExtensions implements CelCompilerLibrary, CelRuntimeLibrary {
34+
private static final Encoder BASE64_ENCODER = Base64.getEncoder();
35+
36+
private static final Decoder BASE64_DECODER = Base64.getDecoder();
37+
38+
@Override
39+
public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
40+
checkerBuilder.addFunctionDeclarations(
41+
CelFunctionDecl.newFunctionDeclaration(
42+
"base64.decode",
43+
CelOverloadDecl.newGlobalOverload(
44+
"base64_decode_string", SimpleType.BYTES, SimpleType.STRING)),
45+
CelFunctionDecl.newFunctionDeclaration(
46+
"base64.encode",
47+
CelOverloadDecl.newGlobalOverload(
48+
"base64_encode_bytes", SimpleType.STRING, SimpleType.BYTES)));
49+
}
50+
51+
@SuppressWarnings("Immutable") // Instances of java.util.Base64 are immutable
52+
@Override
53+
public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
54+
runtimeBuilder.addFunctionBindings(
55+
CelRuntime.CelFunctionBinding.from(
56+
"base64_decode_string",
57+
String.class,
58+
str -> ByteString.copyFrom(BASE64_DECODER.decode(str))),
59+
CelRuntime.CelFunctionBinding.from(
60+
"base64_encode_bytes",
61+
ByteString.class,
62+
bytes -> BASE64_ENCODER.encodeToString(bytes.toByteArray())));
63+
}
64+
}

extensions/src/main/java/dev/cel/extensions/CelExtensions.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public final class CelExtensions {
3030
private static final CelProtoExtensions PROTO_EXTENSIONS = new CelProtoExtensions();
3131
private static final CelBindingsExtensions BINDINGS_EXTENSIONS = new CelBindingsExtensions();
3232

33+
private static final CelEncoderExtensions ENCODER_EXTENSIONS = new CelEncoderExtensions();
34+
3335
/**
3436
* Extended functions for string manipulation.
3537
*
@@ -131,7 +133,7 @@ public static CelMathExtensions math(
131133
* <p>This adds {@code math.greatest} and {@code math.least}. See README.md for their
132134
* documentation.
133135
*
134-
* <p>Note, all macros use the 'math' namespace; however, at the time of macro * expansion the
136+
* <p>Note, all macros use the 'math' namespace; however, at the time of macro expansion the
135137
* namespace looks just like any other identifier. If you are currently using a variable named
136138
* 'math', the macro will likely work just as intended; however, there is some chance for
137139
* collision.
@@ -159,5 +161,15 @@ public static CelBindingsExtensions bindings() {
159161
return BINDINGS_EXTENSIONS;
160162
}
161163

164+
/**
165+
* Extended functions for string, byte and object encodings.
166+
*
167+
* <p>This adds {@code base64.encode} and {@code base64.decode} functions. See README.md for their
168+
* documentation.
169+
*/
170+
public static CelEncoderExtensions encoders() {
171+
return ENCODER_EXTENSIONS;
172+
}
173+
162174
private CelExtensions() {}
163175
}

extensions/src/main/java/dev/cel/extensions/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,36 @@ Examples:
306306

307307
'TacoCat'.upperAscii() // returns 'TACOCAT'
308308
'TacoCÆt Xii'.upperAscii() // returns 'TACOCÆT XII'
309+
310+
## Encoders
311+
312+
Encoding utilities for marshalling data into standardized representations.
313+
314+
### Base64.Decode
315+
316+
Decodes base64-encoded string to bytes.
317+
318+
This function will return an error if the string input is not
319+
base64-encoded.
320+
321+
base64.decode(<string>) -> <bytes>
322+
323+
Examples:
324+
325+
base64.decode('aGVsbG8=') // return b'hello'
326+
base64.decode('aGVsbG8') // return b'hello'. Note that the padding
327+
// character can be omitted.
328+
base64.decode('z!') // error
329+
330+
### Base64.Encode
331+
332+
Encodes bytes to a base64-encoded string. Note that the string is encoded in
333+
ISO_8859_1.
334+
335+
base64.encode(<bytes>) -> <string>
336+
337+
Example:
338+
339+
base64.encode(b'hello') // return 'aGVsbG8='
340+
341+
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.extensions;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static java.nio.charset.StandardCharsets.ISO_8859_1;
19+
import static org.junit.Assert.assertThrows;
20+
21+
import com.google.common.collect.ImmutableMap;
22+
import com.google.protobuf.ByteString;
23+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
24+
import dev.cel.common.CelAbstractSyntaxTree;
25+
import dev.cel.common.CelValidationException;
26+
import dev.cel.common.types.SimpleType;
27+
import dev.cel.compiler.CelCompiler;
28+
import dev.cel.compiler.CelCompilerFactory;
29+
import dev.cel.runtime.CelEvaluationException;
30+
import dev.cel.runtime.CelRuntime;
31+
import dev.cel.runtime.CelRuntimeFactory;
32+
import org.junit.Test;
33+
import org.junit.runner.RunWith;
34+
35+
@RunWith(TestParameterInjector.class)
36+
public class CelEncoderExtensionsTest {
37+
38+
private static final CelCompiler CEL_COMPILER =
39+
CelCompilerFactory.standardCelCompilerBuilder()
40+
.addVar("stringVar", SimpleType.STRING)
41+
.addLibraries(CelExtensions.encoders())
42+
.build();
43+
private static final CelRuntime CEL_RUNTIME =
44+
CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.encoders()).build();
45+
46+
@Test
47+
public void encode_success() throws Exception {
48+
String encodedBytes =
49+
(String)
50+
CEL_RUNTIME
51+
.createProgram(CEL_COMPILER.compile("base64.encode(b'hello')").getAst())
52+
.eval();
53+
54+
assertThat(encodedBytes).isEqualTo("aGVsbG8=");
55+
}
56+
57+
@Test
58+
public void decode_success() throws Exception {
59+
ByteString decodedBytes =
60+
(ByteString)
61+
CEL_RUNTIME
62+
.createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8=')").getAst())
63+
.eval();
64+
65+
assertThat(decodedBytes.size()).isEqualTo(5);
66+
assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("hello");
67+
}
68+
69+
@Test
70+
public void decode_withoutPadding_success() throws Exception {
71+
ByteString decodedBytes =
72+
(ByteString)
73+
CEL_RUNTIME
74+
// RFC2045 6.8, padding can be ignored.
75+
.createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8')").getAst())
76+
.eval();
77+
78+
assertThat(decodedBytes.size()).isEqualTo(5);
79+
assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("hello");
80+
}
81+
82+
@Test
83+
public void roundTrip_success() throws Exception {
84+
String encodedString =
85+
(String)
86+
CEL_RUNTIME
87+
.createProgram(CEL_COMPILER.compile("base64.encode(b'Hello World!')").getAst())
88+
.eval();
89+
ByteString decodedBytes =
90+
(ByteString)
91+
CEL_RUNTIME
92+
.createProgram(CEL_COMPILER.compile("base64.decode(stringVar)").getAst())
93+
.eval(ImmutableMap.of("stringVar", encodedString));
94+
95+
assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("Hello World!");
96+
}
97+
98+
@Test
99+
public void encode_invalidParam_throwsCompilationException() {
100+
CelValidationException e =
101+
assertThrows(
102+
CelValidationException.class,
103+
() -> CEL_COMPILER.compile("base64.encode('hello')").getAst());
104+
105+
assertThat(e).hasMessageThat().contains("found no matching overload for 'base64.encode'");
106+
}
107+
108+
@Test
109+
public void decode_invalidParam_throwsCompilationException() {
110+
CelValidationException e =
111+
assertThrows(
112+
CelValidationException.class,
113+
() -> CEL_COMPILER.compile("base64.decode(b'aGVsbG8=')").getAst());
114+
115+
assertThat(e).hasMessageThat().contains("found no matching overload for 'base64.decode'");
116+
}
117+
118+
@Test
119+
public void decode_malformedBase64Char_throwsEvaluationException() throws Exception {
120+
CelAbstractSyntaxTree ast = CEL_COMPILER.compile("base64.decode('z!')").getAst();
121+
122+
CelEvaluationException e =
123+
assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval());
124+
125+
assertThat(e)
126+
.hasMessageThat()
127+
.contains("Function 'base64_decode_string' failed with arg(s) 'z!'");
128+
assertThat(e).hasCauseThat().hasMessageThat().contains("Illegal base64 character");
129+
}
130+
}

extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ public void addAllStringExtensions_success() throws Exception {
4444
+ " 'hello'.replace('he', 'we') == 'wello' && 'hello'.upperAscii() == 'HELLO' && '"
4545
+ " hello '.trim() == 'hello' && ['he','llo'].join() == 'hello' && 'hi'.split('') =="
4646
+ " ['h','i']";
47-
CelRuntime.Program program = cel.createProgram(cel.compile(allStringExtExpr).getAst());
4847

49-
Object evaluatedResult = program.eval();
48+
boolean evaluatedResult =
49+
(boolean) cel.createProgram(cel.compile(allStringExtExpr).getAst()).eval();
5050

51-
assertThat(evaluatedResult).isEqualTo(true);
51+
assertThat(evaluatedResult).isTrue();
5252
}
5353

5454
@Test
@@ -59,13 +59,14 @@ public void addSubsetOfStringExtensions_success() throws Exception {
5959
.addCompilerLibraries(extensions)
6060
.addRuntimeLibraries(extensions)
6161
.build();
62-
CelRuntime.Program program =
63-
cel.createProgram(
64-
cel.compile("'test'.substring(2) == 'st' && 'hello'.charAt(1) == 'e'").getAst());
6562

66-
Object evaluatedResult = program.eval();
63+
boolean evaluatedResult =
64+
(boolean)
65+
cel.createProgram(
66+
cel.compile("'test'.substring(2) == 'st' && 'hello'.charAt(1) == 'e'").getAst())
67+
.eval();
6768

68-
assertThat(evaluatedResult).isEqualTo(true);
69+
assertThat(evaluatedResult).isTrue();
6970
}
7071

7172
@Test
@@ -101,10 +102,10 @@ public void addAllMathExtensions_success() throws Exception {
101102
.build();
102103
String allMathExtExpr = "math.greatest(1, 2.0) == 2.0 && math.least(1, 2.0) == 1";
103104

104-
CelRuntime.Program program = cel.createProgram(cel.compile(allMathExtExpr).getAst());
105-
Object evaluatedResult = program.eval();
105+
boolean evaluatedResult =
106+
(boolean) cel.createProgram(cel.compile(allMathExtExpr).getAst()).eval();
106107

107-
assertThat(evaluatedResult).isEqualTo(true);
108+
assertThat(evaluatedResult).isTrue();
108109
}
109110

110111
@Test
@@ -116,11 +117,25 @@ public void addSubsetOfMathExtensions_success() throws Exception {
116117
.addRuntimeLibraries(CelExtensions.math(celOptions, CelMathExtensions.Function.MAX))
117118
.build();
118119

119-
CelRuntime.Program program =
120-
cel.createProgram(cel.compile("math.greatest(1, 2.0) == 2.0").getAst());
121-
Object evaluatedResult = program.eval();
120+
boolean evaluatedResult =
121+
(boolean) cel.createProgram(cel.compile("math.greatest(1, 2.0) == 2.0").getAst()).eval();
122122

123-
assertThat(evaluatedResult).isEqualTo(true);
123+
assertThat(evaluatedResult).isTrue();
124124
assertThrows(CelValidationException.class, () -> cel.compile("math.least(1,2)").getAst());
125125
}
126+
127+
@Test
128+
public void addEncoderExtension_success() throws Exception {
129+
Cel cel =
130+
CelFactory.standardCelBuilder()
131+
.addCompilerLibraries(CelExtensions.encoders())
132+
.addRuntimeLibraries(CelExtensions.encoders())
133+
.build();
134+
135+
boolean evaluatedResult =
136+
(boolean)
137+
cel.createProgram(cel.compile("base64.decode('aGVsbG8=') == b'hello'").getAst()).eval();
138+
139+
assertThat(evaluatedResult).isTrue();
140+
}
126141
}

0 commit comments

Comments
 (0)