Skip to content

Commit 258b84a

Browse files
committed
Strip whitespace when decoding base64 #2507
1 parent e056b91 commit 258b84a

File tree

4 files changed

+385
-23
lines changed

4 files changed

+385
-23
lines changed

pkg/yqlib/base64_test.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
//go:build !yq_nobase64
2+
3+
package yqlib
4+
5+
import (
6+
"bufio"
7+
"fmt"
8+
"testing"
9+
10+
"github.com/mikefarah/yq/v4/test"
11+
)
12+
13+
const base64EncodedSimple = "YSBzcGVjaWFsIHN0cmluZw=="
14+
const base64DecodedSimpleExtraSpaces = "\n " + base64EncodedSimple + " \n"
15+
const base64DecodedSimple = "a special string"
16+
17+
const base64EncodedUTF8 = "V29ya3Mgd2l0aCBVVEYtMTYg8J+Yig=="
18+
const base64DecodedUTF8 = "Works with UTF-16 😊"
19+
20+
const base64EncodedYaml = "YTogYXBwbGUK"
21+
const base64DecodedYaml = "a: apple\n"
22+
23+
const base64EncodedEmpty = ""
24+
const base64DecodedEmpty = ""
25+
26+
const base64MissingPadding = "Y2F0cw"
27+
const base64DecodedMissingPadding = "cats"
28+
29+
const base64EncodedCats = "Y2F0cw=="
30+
const base64DecodedCats = "cats"
31+
32+
var base64Scenarios = []formatScenario{
33+
{
34+
skipDoc: true,
35+
description: "empty decode",
36+
input: base64EncodedEmpty,
37+
expected: base64DecodedEmpty + "\n",
38+
scenarioType: "decode",
39+
},
40+
{
41+
skipDoc: true,
42+
description: "simple decode",
43+
input: base64EncodedSimple,
44+
expected: base64DecodedSimple + "\n",
45+
scenarioType: "decode",
46+
},
47+
{
48+
description: "Decode base64: simple",
49+
subdescription: "Decoded data is assumed to be a string.",
50+
input: base64EncodedSimple,
51+
expected: base64DecodedSimple + "\n",
52+
scenarioType: "decode",
53+
},
54+
{
55+
description: "Decode base64: UTF-8",
56+
subdescription: "Base64 decoding supports UTF-8 encoded strings.",
57+
input: base64EncodedUTF8,
58+
expected: base64DecodedUTF8 + "\n",
59+
scenarioType: "decode",
60+
},
61+
{
62+
skipDoc: true,
63+
description: "decode missing padding",
64+
input: base64MissingPadding,
65+
expected: base64DecodedMissingPadding + "\n",
66+
scenarioType: "decode",
67+
},
68+
{
69+
70+
description: "Decode with extra spaces",
71+
subdescription: "Extra leading/trailing whitespace is stripped",
72+
input: base64DecodedSimpleExtraSpaces,
73+
expected: base64DecodedSimple + "\n",
74+
scenarioType: "decode",
75+
},
76+
{
77+
skipDoc: true,
78+
description: "decode with padding",
79+
input: base64EncodedCats,
80+
expected: base64DecodedCats + "\n",
81+
scenarioType: "decode",
82+
},
83+
{
84+
skipDoc: true,
85+
description: "decode yaml document",
86+
input: base64EncodedYaml,
87+
expected: base64DecodedYaml + "\n",
88+
scenarioType: "decode",
89+
},
90+
{
91+
description: "Encode base64: string",
92+
input: "\"" + base64DecodedSimple + "\"",
93+
expected: base64EncodedSimple,
94+
scenarioType: "encode",
95+
},
96+
{
97+
description: "Encode base64: string from document",
98+
subdescription: "Extract a string field and encode it to base64.",
99+
input: "coolData: \"" + base64DecodedSimple + "\"",
100+
expression: ".coolData",
101+
expected: base64EncodedSimple,
102+
scenarioType: "encode",
103+
},
104+
{
105+
skipDoc: true,
106+
description: "encode empty string",
107+
input: "\"\"",
108+
expected: "",
109+
scenarioType: "encode",
110+
},
111+
{
112+
skipDoc: true,
113+
description: "encode UTF-8 string",
114+
input: "\"" + base64DecodedUTF8 + "\"",
115+
expected: base64EncodedUTF8,
116+
scenarioType: "encode",
117+
},
118+
{
119+
skipDoc: true,
120+
description: "encode cats",
121+
input: "\"" + base64DecodedCats + "\"",
122+
expected: base64EncodedCats,
123+
scenarioType: "encode",
124+
},
125+
{
126+
description: "Roundtrip: simple",
127+
skipDoc: true,
128+
input: base64EncodedSimple,
129+
expected: base64EncodedSimple,
130+
scenarioType: "roundtrip",
131+
},
132+
{
133+
description: "Roundtrip: UTF-8",
134+
skipDoc: true,
135+
input: base64EncodedUTF8,
136+
expected: base64EncodedUTF8,
137+
scenarioType: "roundtrip",
138+
},
139+
{
140+
description: "Roundtrip: missing padding",
141+
skipDoc: true,
142+
input: base64MissingPadding,
143+
expected: base64EncodedCats,
144+
scenarioType: "roundtrip",
145+
},
146+
{
147+
description: "Roundtrip: empty",
148+
skipDoc: true,
149+
input: base64EncodedEmpty,
150+
expected: base64EncodedEmpty,
151+
scenarioType: "roundtrip",
152+
},
153+
{
154+
description: "Encode error: non-string",
155+
skipDoc: true,
156+
input: "123",
157+
expectedError: "cannot encode !!int as base64, can only operate on strings",
158+
scenarioType: "encode-error",
159+
},
160+
{
161+
description: "Encode error: array",
162+
skipDoc: true,
163+
input: "[1, 2, 3]",
164+
expectedError: "cannot encode !!seq as base64, can only operate on strings",
165+
scenarioType: "encode-error",
166+
},
167+
{
168+
description: "Encode error: map",
169+
skipDoc: true,
170+
input: "{b: c}",
171+
expectedError: "cannot encode !!map as base64, can only operate on strings",
172+
scenarioType: "encode-error",
173+
},
174+
}
175+
176+
func testBase64Scenario(t *testing.T, s formatScenario) {
177+
switch s.scenarioType {
178+
case "", "decode":
179+
yamlPrefs := ConfiguredYamlPreferences.Copy()
180+
yamlPrefs.Indent = 4
181+
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewBase64Decoder(), NewYamlEncoder(yamlPrefs)), s.description)
182+
case "encode":
183+
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder()), s.description)
184+
case "roundtrip":
185+
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewBase64Decoder(), NewBase64Encoder()), s.description)
186+
case "encode-error":
187+
result, err := processFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder())
188+
if err == nil {
189+
t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result)
190+
} else {
191+
test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.description)
192+
}
193+
194+
default:
195+
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
196+
}
197+
}
198+
199+
func documentBase64Scenario(_ *testing.T, w *bufio.Writer, i interface{}) {
200+
s := i.(formatScenario)
201+
202+
if s.skipDoc {
203+
return
204+
}
205+
switch s.scenarioType {
206+
case "", "decode":
207+
documentBase64DecodeScenario(w, s)
208+
case "encode":
209+
documentBase64EncodeScenario(w, s)
210+
211+
default:
212+
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
213+
}
214+
}
215+
216+
func documentBase64DecodeScenario(w *bufio.Writer, s formatScenario) {
217+
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
218+
219+
if s.subdescription != "" {
220+
writeOrPanic(w, s.subdescription)
221+
writeOrPanic(w, "\n\n")
222+
}
223+
224+
writeOrPanic(w, "Given a sample.txt file of:\n")
225+
writeOrPanic(w, fmt.Sprintf("```\n%v\n```\n", s.input))
226+
227+
writeOrPanic(w, "then\n")
228+
expression := s.expression
229+
if expression == "" {
230+
expression = "."
231+
}
232+
writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=base64 -oy '%v' sample.txt\n```\n", expression))
233+
writeOrPanic(w, "will output\n")
234+
235+
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewBase64Decoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
236+
}
237+
238+
func documentBase64EncodeScenario(w *bufio.Writer, s formatScenario) {
239+
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
240+
241+
if s.subdescription != "" {
242+
writeOrPanic(w, s.subdescription)
243+
writeOrPanic(w, "\n\n")
244+
}
245+
246+
writeOrPanic(w, "Given a sample.yml file of:\n")
247+
writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.input))
248+
249+
writeOrPanic(w, "then\n")
250+
expression := s.expression
251+
if expression == "" {
252+
expression = "."
253+
}
254+
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=base64 '%v' sample.yml\n```\n", expression))
255+
writeOrPanic(w, "will output\n")
256+
257+
writeOrPanic(w, fmt.Sprintf("```\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder())))
258+
}
259+
260+
func TestBase64Scenarios(t *testing.T) {
261+
for _, tt := range base64Scenarios {
262+
testBase64Scenario(t, tt)
263+
}
264+
genericScenarios := make([]interface{}, len(base64Scenarios))
265+
for i, s := range base64Scenarios {
266+
genericScenarios[i] = s
267+
}
268+
documentScenarios(t, "usage", "base64", genericScenarios, documentBase64Scenario)
269+
}

pkg/yqlib/decoder_base64.go

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,6 @@ import (
99
"strings"
1010
)
1111

12-
type base64Padder struct {
13-
count int
14-
io.Reader
15-
}
16-
17-
func (c *base64Padder) pad(buf []byte) (int, error) {
18-
pad := strings.Repeat("=", (4 - c.count%4))
19-
n, err := strings.NewReader(pad).Read(buf)
20-
c.count += n
21-
return n, err
22-
}
23-
24-
func (c *base64Padder) Read(buf []byte) (int, error) {
25-
n, err := c.Reader.Read(buf)
26-
c.count += n
27-
28-
if err == io.EOF && c.count%4 != 0 {
29-
return c.pad(buf)
30-
}
31-
return n, err
32-
}
33-
3412
type base64Decoder struct {
3513
reader io.Reader
3614
finished bool
@@ -43,7 +21,25 @@ func NewBase64Decoder() Decoder {
4321
}
4422

4523
func (dec *base64Decoder) Init(reader io.Reader) error {
46-
dec.reader = &base64Padder{Reader: reader}
24+
// Read all data from the reader and strip leading/trailing whitespace
25+
// This is necessary because base64 decoding needs to see the complete input
26+
// to handle padding correctly, and we need to strip whitespace before decoding.
27+
buf := new(bytes.Buffer)
28+
if _, err := buf.ReadFrom(reader); err != nil {
29+
return err
30+
}
31+
32+
// Strip leading and trailing whitespace
33+
stripped := strings.TrimSpace(buf.String())
34+
35+
// Add padding if needed (base64 strings should be a multiple of 4 characters)
36+
padLen := len(stripped) % 4
37+
if padLen > 0 {
38+
stripped += strings.Repeat("=", 4-padLen)
39+
}
40+
41+
// Create a new reader from the stripped and padded data
42+
dec.reader = strings.NewReader(stripped)
4743
dec.readAnything = false
4844
dec.finished = false
4945
return nil

0 commit comments

Comments
 (0)