Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement(remap): Add charset parameter to encode_base64 function #6065

Merged
merged 7 commits into from Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 28 additions & 5 deletions docs/reference/remap/encode_base64.cue
Expand Up @@ -15,13 +15,25 @@ remap: functions: encode_base64: {
type: ["boolean"]
default: true
},
{
name: "charset"
description: ""
required: false
type: ["string"]
default: "standard"
enum: {
standard: "[Standard](\(urls.base64_standard)) Base64 format."
url_safe: "Modified Base64 for [URL variants](\(urls.base64_url_safe)."
}
},
]
internal_failure_reason: null
return: ["string"]
category: "Encode"
description: """
Encodes the provided `value` to [Base64](\(urls.base64)) using the "standard" character set (`+` and `/`).
"""
category: "Encode"
description: #"""
Encodes the provided `value` to [Base64](\(urls.base64)) either padded or non-padded and
using the specified character set.
"""#
examples: [
{
title: "Encode string"
Expand All @@ -39,11 +51,22 @@ remap: functions: encode_base64: {
input: {
message: "please encode me, no padding though"
}
source: ".encoded = encode_base64(.message, padding = false)"
source: ".encoded = encode_base64(.message, padding: false)"
output: {
message: "please encode me, no padding though"
encoded: "cGxlYXNlIGVuY29kZSBtZSwgbm8gcGFkZGluZyB0aG91Z2g"
}
},
{
title: "Encode URL string"
input: {
message: "please encode me, but safe for URLs"
}
source: #".encoded = encode_base64(.message, charset: "url_safe")"#
output: {
message: "please encode me, but safe for URLs"
encoded: "cGxlYXNlIGVuY29kZSBtZSwgYnV0IHNhZmUgZm9yIFVSTHM="
}
},
]
}
2 changes: 2 additions & 0 deletions docs/reference/urls.cue
Expand Up @@ -77,6 +77,8 @@ urls: {
azure_monitor_logs_endpoints: "https://docs.microsoft.com/en-us/rest/api/monitor/"
base64: "https://en.wikipedia.org/wiki/Base64"
base64_padding: "https://en.wikipedia.org/wiki/Base64#Output_padding"
base64_standard: "https://tools.ietf.org/html/rfc4648#section-4"
base64_url_safe: "https://en.wikipedia.org/wiki/Base64#URL_applications"
basic_auth: "https://en.wikipedia.org/wiki/Basic_access_authentication"
big_query_streaming: "https://cloud.google.com/bigquery/streaming-data-into-bigquery"
cargo_audit: "https://github.com/RustSec/cargo-audit"
Expand Down
142 changes: 115 additions & 27 deletions lib/remap-functions/src/encode_base64.rs
@@ -1,4 +1,5 @@
use remap::prelude::*;
use std::str::FromStr;

#[derive(Clone, Copy, Debug)]
pub struct EncodeBase64;
Expand All @@ -20,21 +21,69 @@ impl Function for EncodeBase64 {
accepts: |v| matches!(v, Value::Boolean(_)),
required: false,
},
Parameter {
keyword: "charset",
accepts: |v| matches!(v, Value::Bytes(_)),
required: false,
},
]
}

fn compile(&self, mut arguments: ArgumentList) -> Result<Box<dyn Expression>> {
let value = arguments.required("value")?.boxed();
let padding = arguments.optional("padding").map(Expr::boxed);
let charset = arguments.optional("charset").map(Expr::boxed);

Ok(Box::new(EncodeBase64Fn { value, padding }))
Ok(Box::new(EncodeBase64Fn {
value,
padding,
charset,
}))
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Charset {
Standard,
UrlSafe,
}

impl Default for Charset {
fn default() -> Self {
Self::Standard
}
}

impl Into<base64::CharacterSet> for Charset {
fn into(self) -> base64::CharacterSet {
use Charset::*;

match self {
Standard => base64::CharacterSet::Standard,
UrlSafe => base64::CharacterSet::UrlSafe,
}
}
}

impl FromStr for Charset {
type Err = &'static str;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
use Charset::*;

match s {
"standard" => Ok(Standard),
"url_safe" => Ok(UrlSafe),
_ => Err("unknown charset"),
}
}
}

#[derive(Clone, Debug)]
struct EncodeBase64Fn {
value: Box<dyn Expression>,
padding: Option<Box<dyn Expression>>,
charset: Option<Box<dyn Expression>>,
}

impl Expression for EncodeBase64Fn {
Expand All @@ -51,11 +100,19 @@ impl Expression for EncodeBase64Fn {
.transpose()?
.unwrap_or(true);

let config = if padding {
base64::STANDARD
} else {
base64::STANDARD_NO_PAD
};
let charset = self
.charset
.as_ref()
.map(|c| {
c.execute(state, object)
.and_then(|v| Value::try_bytes(v).map_err(Into::into))
})
.transpose()?
.map(|c| Charset::from_str(&String::from_utf8_lossy(&c)))
.transpose()?
.unwrap_or_default();

let config = base64::Config::new(charset.into(), padding);

Ok(base64::encode_config(value, config).into())
}
Expand All @@ -68,10 +125,16 @@ impl Expression for EncodeBase64Fn {
.as_ref()
.map(|padding| padding.type_def(state).fallible_unless(Kind::Boolean));

let charset_def = self
.charset
.as_ref()
.map(|charset| charset.type_def(state).into_fallible(true));

self.value
.type_def(state)
.fallible_unless(Kind::Bytes)
.merge_optional(padding_def)
.merge_optional(charset_def)
.with_constraint(Kind::Bytes)
}
}
Expand All @@ -82,42 +145,47 @@ mod test {
use value::Kind;

test_type_def![
value_string_padding_unspecified_infallible {
value_string_padding_unspecified_charset_unspecified_infallible {
expr: |_| EncodeBase64Fn {
value: Literal::from("foo").boxed(),
value: lit!("foo").boxed(),
padding: None,
charset: None,
},
def: TypeDef { kind: Kind::Bytes, ..Default::default() },
}

value_string_padding_boolean_infallible {
valid_charset_fallible {
expr: |_| EncodeBase64Fn {
value: Literal::from("foo").boxed(),
padding: Some(Literal::from(false).boxed()),
value: lit!("foo").boxed(),
padding: Some(lit!(false).boxed()),
charset: Some(lit!("standard").boxed()),
},
def: TypeDef { kind: Kind::Bytes, ..Default::default() },
def: TypeDef { fallible: true, kind: Kind::Bytes, ..Default::default() },
}

value_string_padding_non_boolean_fallible {
padding_non_boolean_fallible {
expr: |_| EncodeBase64Fn {
value: Literal::from("foo").boxed(),
padding: Some(Literal::from("foo").boxed()),
value: lit!("foo").boxed(),
padding: Some(lit!("foo").boxed()),
charset: None,
},
def: TypeDef { fallible: true, kind: Kind::Bytes, ..Default::default() },
}

value_non_string_fallible {
expr: |_| EncodeBase64Fn {
value: Literal::from(127).boxed(),
padding: Some(Literal::from(true).boxed()),
padding: None,
charset: None,
},
def: TypeDef { fallible: true, kind: Kind::Bytes, ..Default::default() },
}

both_types_wrong_fallible {
all_types_wrong_fallible {
expr: |_| EncodeBase64Fn {
value: Literal::from(127).boxed(),
padding: Some(Literal::from("foo").boxed()),
padding: Some(lit!("foo").boxed()),
charset: Some(lit!(127).boxed()),
},
def: TypeDef { fallible: true, kind: Kind::Bytes, ..Default::default() },
}
Expand All @@ -126,24 +194,44 @@ mod test {
test_function![
encode_base64 => EncodeBase64;

string_value_with_padding {
args: func_args![value: value!("some string value"), padding: value!(true)],
with_defaults {
args: func_args![value: value!("some string value")],
want: Ok(value!("c29tZSBzdHJpbmcgdmFsdWU=")),
}

string_value_with_default_padding {
args: func_args![value: value!("some string value")],
with_padding_standard_charset {
args: func_args![value: value!("some string value"), padding: value!(true), charset: value!("standard")],
want: Ok(value!("c29tZSBzdHJpbmcgdmFsdWU=")),
}

string_value_no_padding {
args: func_args![value: value!("some string value"), padding: value!(false)],
want: Ok(value!("c29tZSBzdHJpbmcgdmFsdWU")),
no_padding_standard_charset {
args: func_args![value: value!("some+string+value"), padding: value!(false), charset: value!("standard")],
want: Ok(value!("c29tZStzdHJpbmcrdmFsdWU")),
}

no_padding_urlsafe_charset {
args: func_args![value: value!("some+string+value"), padding: value!(false), charset: value!("url_safe")],
want: Ok(value!("c29tZStzdHJpbmcrdmFsdWU")),
}

with_padding_urlsafe_charset {
args: func_args![value: value!("foo+bar+baz"), padding: value!(false), charset: value!("url_safe")],
want: Ok(value!("Zm9vK2JhcitiYXo")),
}

empty_string_value {
args: func_args![value: value!("")],
empty_string_standard_charset {
args: func_args![value: value!(""), charset: value!("standard")],
want: Ok(value!("")),
}

empty_string_urlsafe_charset {
args: func_args![value: value!(""), charset: value!("url_safe")],
want: Ok(value!("")),
}

invalid_charset_error {
args: func_args![value: value!("some string value"), padding: value!(false), charset: value!("foo")],
want: Err("function call error: unknown charset"),
}
];
}