feat: add @base64url/@base64urld operators (RFC 4648 §5) (#2827)

Add the URL- and filename-safe Base64 variant alongside the existing
@base64/@base64d operators. The URL-safe alphabet replaces + and / with
- and _ so encoded values can be embedded in URLs, filenames, or other
contexts where + and / have special meaning.

Adds:
- Base64UrlFormat registered in the Formats slice (also exposes
  base64url as a top-level --input/--output-format)
- NewBase64URLEncoder / NewBase64URLDecoder backed by base64.URLEncoding
- @base64url / @base64urld lexer tokens (placed before @base64/@base64d
  so the longer patterns match first under Go's leftmost-first regex)
- no_base64.go stubs for the yq_nobase64 build tag
- operator tests covering encode, decode, round-trip, and missing-padding
  decode

Closes #2794

Co-authored-by: Pablo Garcia <[email protected]>
This commit is contained in:
MsfPablo
2026-09-15 15:54:05 +10:00
committed by GitHub
co-authored by Pablo Garcia
parent e27a15e50a
commit 5efa5ffcd8
7 changed files with 97 additions and 0 deletions
+7
View File
@@ -20,6 +20,13 @@ func NewBase64Decoder() Decoder {
return &base64Decoder{finished: false, encoding: *base64.StdEncoding}
}
// NewBase64URLDecoder returns a decoder for the URL- and filename-safe
// Base64 variant (RFC 4648 §5): `-` and `_` replace `+` and `/`.
// The shared padding logic in Init tolerates inputs with or without padding.
func NewBase64URLDecoder() Decoder {
return &base64Decoder{finished: false, encoding: *base64.URLEncoding}
}
func (dec *base64Decoder) Init(reader io.Reader) error {
// Read all data from the reader and strip leading/trailing whitespace
// This is necessary because base64 decoding needs to see the complete input
+30
View File
@@ -514,3 +514,33 @@ coolData:
a: apple
```
## Encode a string to base64url
Given a sample.yml file of:
```yaml
coolData: "Works with UTF-16 \U0001F60A"
```
then
```bash
yq '.coolData | @base64url' sample.yml
```
will output
```yaml
V29ya3Mgd2l0aCBVVEYtMTYg8J-Yig==
```
## Decode a base64url encoded string
URL-safe variant uses `-` and `_` in place of `+` and `/` (RFC 4648 §5).
Given a sample.yml file of:
```yaml
coolData: V29ya3Mgd2l0aCBVVEYtMTYg8J-Yig==
```
then
```bash
yq '.coolData | @base64urld' sample.yml
```
will output
```yaml
Works with UTF-16 😊
```
+6
View File
@@ -16,6 +16,12 @@ func NewBase64Encoder() Encoder {
return &base64Encoder{encoding: *base64.StdEncoding}
}
// NewBase64URLEncoder returns an encoder for the URL- and filename-safe
// Base64 variant (RFC 4648 §5): `-` and `_` replace `+` and `/`.
func NewBase64URLEncoder() Encoder {
return &base64Encoder{encoding: *base64.URLEncoding}
}
func (e *base64Encoder) CanHandleAliases() bool {
return false
}
+10
View File
@@ -58,6 +58,15 @@ var Base64Format = &Format{"base64", []string{},
func() Decoder { return NewBase64Decoder() },
}
// Base64UrlFormat is the URL- and filename-safe Base64 variant defined by
// RFC 4648 §5: it uses `-` and `_` in place of `+` and `/`. Padding is kept
// (matching the standard Base64Format), so round-tripping with @base64urld
// works whether or not the input was padded.
var Base64UrlFormat = &Format{"base64url", []string{},
func() Encoder { return NewBase64URLEncoder() },
func() Decoder { return NewBase64URLDecoder() },
}
var UriFormat = &Format{"uri", []string{},
func() Encoder { return NewUriEncoder() },
func() Decoder { return NewUriDecoder() },
@@ -102,6 +111,7 @@ var Formats = []*Format{
TSVFormat,
XMLFormat,
Base64Format,
Base64UrlFormat,
UriFormat,
ShFormat,
TomlFormat,
+3
View File
@@ -81,6 +81,9 @@ var participleYqRules = []*participleYqRule{
{"TSVDecode", `from_?tsv|@tsvd`, decodeOp(TSVFormat), 0},
{"TSVEncode", `to_?tsv|@tsv`, encodeWithIndent(TSVFormat, 0), 0},
{"Base64Urld", `@base64urld`, decodeOp(Base64UrlFormat), 0},
{"Base64Url", `@base64url`, encodeWithIndent(Base64UrlFormat, 0), 0},
{"Base64d", `@base64d`, decodeOp(Base64Format), 0},
{"Base64", `@base64`, encodeWithIndent(Base64Format, 0), 0},
+8
View File
@@ -9,3 +9,11 @@ func NewBase64Decoder() Decoder {
func NewBase64Encoder() Encoder {
return nil
}
func NewBase64URLDecoder() Decoder {
return nil
}
func NewBase64URLEncoder() Encoder {
return nil
}
@@ -326,6 +326,39 @@ var encoderDecoderOperatorScenarios = []expressionScenario{
"D0, P[], (!!str)::cats\n",
},
},
{
description: "Encode a string to base64url",
document: "coolData: Works with UTF-16 😊",
expression: ".coolData | @base64url",
expected: []string{
"D0, P[coolData], (!!str)::V29ya3Mgd2l0aCBVVEYtMTYg8J-Yig==\n",
},
},
{
description: "Decode a base64url encoded string",
subdescription: "URL-safe variant uses `-` and `_` in place of `+` and `/` (RFC 4648 §5).",
document: "coolData: V29ya3Mgd2l0aCBVVEYtMTYg8J-Yig==",
expression: ".coolData | @base64urld",
expected: []string{
"D0, P[coolData], (!!str)::Works with UTF-16 😊\n",
},
},
{
description: "base64url round trip preserves bytes with + and /",
skipDoc: true,
expression: `">>>???" | @base64url | @base64urld`,
expected: []string{
"D0, P[], (!!str)::>>>???\n",
},
},
{
description: "base64url missing padding decode",
skipDoc: true,
expression: `"V29ya3Mgd2l0aCBVVEYtMTYg8J-Yig" | @base64urld`,
expected: []string{
"D0, P[], (!!str)::Works with UTF-16 😊\n",
},
},
{
requiresFormat: "xml",
description: "empty xml decode",