Skip to content

Commit

Permalink
Add encoders extension library with base64 encoding/decoding capabili…
Browse files Browse the repository at this point in the history
…ties

PiperOrigin-RevId: 552931694
  • Loading branch information
l46kok authored and copybara-github committed Aug 1, 2023
1 parent df15385 commit fa6bfa1
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 16 deletions.
15 changes: 15 additions & 0 deletions extensions/src/main/java/dev/cel/extensions/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ java_library(
],
deps = [
":bindings",
":encoders",
":math",
":protos",
":strings",
Expand Down Expand Up @@ -90,6 +91,20 @@ java_library(
],
)

java_library(
name = "encoders",
srcs = ["CelEncoderExtensions.java"],
deps = [
"//checker:checker_builder",
"//common:compiler_common",
"//common/types",
"//compiler:compiler_builder",
"//runtime",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_protobuf_protobuf_java",
],
)

java_library(
name = "optional_library",
srcs = ["CelOptionalLibrary.java"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2023 Google LLC
//
// 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
//
// https://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 dev.cel.extensions;

import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.ByteString;
import dev.cel.checker.CelCheckerBuilder;
import dev.cel.common.CelFunctionDecl;
import dev.cel.common.CelOverloadDecl;
import dev.cel.common.types.SimpleType;
import dev.cel.compiler.CelCompilerLibrary;
import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelRuntimeBuilder;
import dev.cel.runtime.CelRuntimeLibrary;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;

/** Internal implementation of Encoder Extensions. */
@Immutable
public class CelEncoderExtensions implements CelCompilerLibrary, CelRuntimeLibrary {
private static final Encoder BASE64_ENCODER = Base64.getEncoder();

private static final Decoder BASE64_DECODER = Base64.getDecoder();

@Override
public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
checkerBuilder.addFunctionDeclarations(
CelFunctionDecl.newFunctionDeclaration(
"base64.decode",
CelOverloadDecl.newGlobalOverload(
"base64_decode_string", SimpleType.BYTES, SimpleType.STRING)),
CelFunctionDecl.newFunctionDeclaration(
"base64.encode",
CelOverloadDecl.newGlobalOverload(
"base64_encode_bytes", SimpleType.STRING, SimpleType.BYTES)));
}

@SuppressWarnings("Immutable") // Instances of java.util.Base64 are immutable
@Override
public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
runtimeBuilder.addFunctionBindings(
CelRuntime.CelFunctionBinding.from(
"base64_decode_string",
String.class,
str -> ByteString.copyFrom(BASE64_DECODER.decode(str))),
CelRuntime.CelFunctionBinding.from(
"base64_encode_bytes",
ByteString.class,
bytes -> BASE64_ENCODER.encodeToString(bytes.toByteArray())));
}
}
14 changes: 13 additions & 1 deletion extensions/src/main/java/dev/cel/extensions/CelExtensions.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public final class CelExtensions {
private static final CelProtoExtensions PROTO_EXTENSIONS = new CelProtoExtensions();
private static final CelBindingsExtensions BINDINGS_EXTENSIONS = new CelBindingsExtensions();

private static final CelEncoderExtensions ENCODER_EXTENSIONS = new CelEncoderExtensions();

/**
* Extended functions for string manipulation.
*
Expand Down Expand Up @@ -131,7 +133,7 @@ public static CelMathExtensions math(
* <p>This adds {@code math.greatest} and {@code math.least}. See README.md for their
* documentation.
*
* <p>Note, all macros use the 'math' namespace; however, at the time of macro * expansion the
* <p>Note, all macros use the 'math' namespace; however, at the time of macro expansion the
* namespace looks just like any other identifier. If you are currently using a variable named
* 'math', the macro will likely work just as intended; however, there is some chance for
* collision.
Expand Down Expand Up @@ -159,5 +161,15 @@ public static CelBindingsExtensions bindings() {
return BINDINGS_EXTENSIONS;
}

/**
* Extended functions for string, byte and object encodings.
*
* <p>This adds {@code base64.encode} and {@code base64.decode} functions. See README.md for their
* documentation.
*/
public static CelEncoderExtensions encoders() {
return ENCODER_EXTENSIONS;
}

private CelExtensions() {}
}
33 changes: 33 additions & 0 deletions extensions/src/main/java/dev/cel/extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,36 @@ Examples:

'TacoCat'.upperAscii() // returns 'TACOCAT'
'TacoCÆt Xii'.upperAscii() // returns 'TACOCÆT XII'

## Encoders

Encoding utilities for marshalling data into standardized representations.

### Base64.Decode

Decodes base64-encoded string to bytes.

This function will return an error if the string input is not
base64-encoded.

base64.decode(<string>) -> <bytes>

Examples:

base64.decode('aGVsbG8=') // return b'hello'
base64.decode('aGVsbG8') // return b'hello'. Note that the padding
// character can be omitted.
base64.decode('z!') // error

### Base64.Encode

Encodes bytes to a base64-encoded string. Note that the string is encoded in
ISO_8859_1.

base64.encode(<bytes>) -> <string>

Example:

base64.encode(b'hello') // return 'aGVsbG8='


Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2023 Google LLC
//
// 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
//
// https://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 dev.cel.extensions;

import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static org.junit.Assert.assertThrows;

import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ByteString;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelValidationException;
import dev.cel.common.types.SimpleType;
import dev.cel.compiler.CelCompiler;
import dev.cel.compiler.CelCompilerFactory;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelRuntimeFactory;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(TestParameterInjector.class)
public class CelEncoderExtensionsTest {

private static final CelCompiler CEL_COMPILER =
CelCompilerFactory.standardCelCompilerBuilder()
.addVar("stringVar", SimpleType.STRING)
.addLibraries(CelExtensions.encoders())
.build();
private static final CelRuntime CEL_RUNTIME =
CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.encoders()).build();

@Test
public void encode_success() throws Exception {
String encodedBytes =
(String)
CEL_RUNTIME
.createProgram(CEL_COMPILER.compile("base64.encode(b'hello')").getAst())
.eval();

assertThat(encodedBytes).isEqualTo("aGVsbG8=");
}

@Test
public void decode_success() throws Exception {
ByteString decodedBytes =
(ByteString)
CEL_RUNTIME
.createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8=')").getAst())
.eval();

assertThat(decodedBytes.size()).isEqualTo(5);
assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("hello");
}

@Test
public void decode_withoutPadding_success() throws Exception {
ByteString decodedBytes =
(ByteString)
CEL_RUNTIME
// RFC2045 6.8, padding can be ignored.
.createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8')").getAst())
.eval();

assertThat(decodedBytes.size()).isEqualTo(5);
assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("hello");
}

@Test
public void roundTrip_success() throws Exception {
String encodedString =
(String)
CEL_RUNTIME
.createProgram(CEL_COMPILER.compile("base64.encode(b'Hello World!')").getAst())
.eval();
ByteString decodedBytes =
(ByteString)
CEL_RUNTIME
.createProgram(CEL_COMPILER.compile("base64.decode(stringVar)").getAst())
.eval(ImmutableMap.of("stringVar", encodedString));

assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("Hello World!");
}

@Test
public void encode_invalidParam_throwsCompilationException() {
CelValidationException e =
assertThrows(
CelValidationException.class,
() -> CEL_COMPILER.compile("base64.encode('hello')").getAst());

assertThat(e).hasMessageThat().contains("found no matching overload for 'base64.encode'");
}

@Test
public void decode_invalidParam_throwsCompilationException() {
CelValidationException e =
assertThrows(
CelValidationException.class,
() -> CEL_COMPILER.compile("base64.decode(b'aGVsbG8=')").getAst());

assertThat(e).hasMessageThat().contains("found no matching overload for 'base64.decode'");
}

@Test
public void decode_malformedBase64Char_throwsEvaluationException() throws Exception {
CelAbstractSyntaxTree ast = CEL_COMPILER.compile("base64.decode('z!')").getAst();

CelEvaluationException e =
assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval());

assertThat(e)
.hasMessageThat()
.contains("Function 'base64_decode_string' failed with arg(s) 'z!'");
assertThat(e).hasCauseThat().hasMessageThat().contains("Illegal base64 character");
}
}
45 changes: 30 additions & 15 deletions extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ public void addAllStringExtensions_success() throws Exception {
+ " 'hello'.replace('he', 'we') == 'wello' && 'hello'.upperAscii() == 'HELLO' && '"
+ " hello '.trim() == 'hello' && ['he','llo'].join() == 'hello' && 'hi'.split('') =="
+ " ['h','i']";
CelRuntime.Program program = cel.createProgram(cel.compile(allStringExtExpr).getAst());

Object evaluatedResult = program.eval();
boolean evaluatedResult =
(boolean) cel.createProgram(cel.compile(allStringExtExpr).getAst()).eval();

assertThat(evaluatedResult).isEqualTo(true);
assertThat(evaluatedResult).isTrue();
}

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

Object evaluatedResult = program.eval();
boolean evaluatedResult =
(boolean)
cel.createProgram(
cel.compile("'test'.substring(2) == 'st' && 'hello'.charAt(1) == 'e'").getAst())
.eval();

assertThat(evaluatedResult).isEqualTo(true);
assertThat(evaluatedResult).isTrue();
}

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

CelRuntime.Program program = cel.createProgram(cel.compile(allMathExtExpr).getAst());
Object evaluatedResult = program.eval();
boolean evaluatedResult =
(boolean) cel.createProgram(cel.compile(allMathExtExpr).getAst()).eval();

assertThat(evaluatedResult).isEqualTo(true);
assertThat(evaluatedResult).isTrue();
}

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

CelRuntime.Program program =
cel.createProgram(cel.compile("math.greatest(1, 2.0) == 2.0").getAst());
Object evaluatedResult = program.eval();
boolean evaluatedResult =
(boolean) cel.createProgram(cel.compile("math.greatest(1, 2.0) == 2.0").getAst()).eval();

assertThat(evaluatedResult).isEqualTo(true);
assertThat(evaluatedResult).isTrue();
assertThrows(CelValidationException.class, () -> cel.compile("math.least(1,2)").getAst());
}

@Test
public void addEncoderExtension_success() throws Exception {
Cel cel =
CelFactory.standardCelBuilder()
.addCompilerLibraries(CelExtensions.encoders())
.addRuntimeLibraries(CelExtensions.encoders())
.build();

boolean evaluatedResult =
(boolean)
cel.createProgram(cel.compile("base64.decode('aGVsbG8=') == b'hello'").getAst()).eval();

assertThat(evaluatedResult).isTrue();
}
}

0 comments on commit fa6bfa1

Please sign in to comment.