/
encoding.go
335 lines (305 loc) · 11.7 KB
/
encoding.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package funcs
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"log"
"net/url"
"unicode/utf8"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"golang.org/x/text/encoding/ianaindex"
)
// Base64DecodeFunc constructs a function that decodes a string containing a base64 sequence.
var Base64DecodeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str, strMarks := args[0].Unmark()
s := str.AsString()
sDec, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data %s", redactIfSensitive(s, strMarks))
}
if !utf8.Valid([]byte(sDec)) {
log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", redactIfSensitive(sDec, strMarks))
return cty.UnknownVal(cty.String), fmt.Errorf("the result of decoding the provided string is not valid UTF-8")
}
return cty.StringVal(string(sDec)).WithMarks(strMarks), nil
},
})
// Base64EncodeFunc constructs a function that encodes a string to a base64 sequence.
var Base64EncodeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.StringVal(base64.StdEncoding.EncodeToString([]byte(args[0].AsString()))), nil
},
})
// TextEncodeBase64Func constructs a function that encodes a string to a target encoding and then to a base64 sequence.
var TextEncodeBase64Func = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "string",
Type: cty.String,
},
{
Name: "encoding",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
encoding, err := ianaindex.IANA.Encoding(args[1].AsString())
if err != nil || encoding == nil {
return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias in this OpenTofu version", args[1].AsString())
}
encName, err := ianaindex.IANA.Name(encoding)
if err != nil { // would be weird, since we just read this encoding out
encName = args[1].AsString()
}
encoder := encoding.NewEncoder()
encodedInput, err := encoder.Bytes([]byte(args[0].AsString()))
if err != nil {
// The string representations of "err" disclose implementation
// details of the underlying library, and the main error we might
// like to return a special message for is unexported as
// golang.org/x/text/encoding/internal.RepertoireError, so this
// is just a generic error message for now.
//
// We also don't include the string itself in the message because
// it can typically be very large, contain newline characters,
// etc.
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains characters that cannot be represented in %s", encName)
}
return cty.StringVal(base64.StdEncoding.EncodeToString(encodedInput)), nil
},
})
// TextDecodeBase64Func constructs a function that decodes a base64 sequence to a target encoding.
var TextDecodeBase64Func = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "source",
Type: cty.String,
},
{
Name: "encoding",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
encoding, err := ianaindex.IANA.Encoding(args[1].AsString())
if err != nil || encoding == nil {
return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias in this OpenTofu version", args[1].AsString())
}
encName, err := ianaindex.IANA.Name(encoding)
if err != nil { // would be weird, since we just read this encoding out
encName = args[1].AsString()
}
s := args[0].AsString()
sDec, err := base64.StdEncoding.DecodeString(s)
if err != nil {
switch err := err.(type) {
case base64.CorruptInputError:
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given value is has an invalid base64 symbol at offset %d", int(err))
default:
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %w", err)
}
}
decoder := encoding.NewDecoder()
decoded, err := decoder.Bytes(sDec)
if err != nil || bytes.ContainsRune(decoded, '�') {
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains symbols that are not defined for %s", encName)
}
return cty.StringVal(string(decoded)), nil
},
})
// Base64GzipFunc constructs a function that compresses a string with gzip and then encodes the result in
// Base64 encoding.
var Base64GzipFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
s := args[0].AsString()
var b bytes.Buffer
gz := gzip.NewWriter(&b)
if _, err := gz.Write([]byte(s)); err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: %w", err)
}
if err := gz.Flush(); err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: %w", err)
}
if err := gz.Close(); err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: %w", err)
}
return cty.StringVal(base64.StdEncoding.EncodeToString(b.Bytes())), nil
},
})
// Base64GunzipFunc constructs a function that Bae64 decodes a string and decompresses the result with gunzip.
var Base64GunzipFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str, strMarks := args[0].Unmark()
s := str.AsString()
sDec, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data %s", redactIfSensitive(s, strMarks))
}
sDecBuffer := bytes.NewReader(sDec)
gzipReader, err := gzip.NewReader(sDecBuffer)
if err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to gunzip bytestream: %w", err)
}
gunzip, err := io.ReadAll(gzipReader)
if err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to read gunzip raw data: %w", err)
}
return cty.StringVal(string(gunzip)), nil
},
})
// URLEncodeFunc constructs a function that applies URL encoding to a given string.
var URLEncodeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.StringVal(url.QueryEscape(args[0].AsString())), nil
},
})
// URLDecodeFunc constructs a function that applies URL decoding to a given encoded string.
var URLDecodeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
query, err := url.QueryUnescape(args[0].AsString())
if err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode URL '%s': %v", query, err)
}
return cty.StringVal(query), nil
},
})
// Base64Decode decodes a string containing a base64 sequence.
//
// OpenTofu uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
//
// Strings in the OpenTofu language are sequences of unicode characters rather
// than bytes, so this function will also interpret the resulting bytes as
// UTF-8. If the bytes after Base64 decoding are _not_ valid UTF-8, this function
// produces an error.
func Base64Decode(str cty.Value) (cty.Value, error) {
return Base64DecodeFunc.Call([]cty.Value{str})
}
// Base64Encode applies Base64 encoding to a string.
//
// OpenTofu uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
//
// Strings in the OpenTofu language are sequences of unicode characters rather
// than bytes, so this function will first encode the characters from the string
// as UTF-8, and then apply Base64 encoding to the result.
func Base64Encode(str cty.Value) (cty.Value, error) {
return Base64EncodeFunc.Call([]cty.Value{str})
}
// Base64Gzip compresses a string with gzip and then encodes the result in
// Base64 encoding.
//
// OpenTofu uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
//
// Strings in the OpenTofu language are sequences of unicode characters rather
// than bytes, so this function will first encode the characters from the string
// as UTF-8, then apply gzip compression, and then finally apply Base64 encoding.
func Base64Gzip(str cty.Value) (cty.Value, error) {
return Base64GzipFunc.Call([]cty.Value{str})
}
// Base64Gunzip decodes a Base64-encoded string and uncompresses the result with gzip.
//
// Opentofu uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
func Base64Gunzip(str cty.Value) (cty.Value, error) {
return Base64GunzipFunc.Call([]cty.Value{str})
}
// URLEncode applies URL encoding to a given string.
//
// This function identifies characters in the given string that would have a
// special meaning when included as a query string argument in a URL and
// escapes them using RFC 3986 "percent encoding".
//
// If the given string contains non-ASCII characters, these are first encoded as
// UTF-8 and then percent encoding is applied separately to each UTF-8 byte.
func URLEncode(str cty.Value) (cty.Value, error) {
return URLEncodeFunc.Call([]cty.Value{str})
}
// URLDecode decodes a URL encoded string.
//
// This function decodes the given string that has been encoded.
//
// If the given string contains non-ASCII characters, these are first encoded as
// UTF-8 and then percent decoding is applied separately to each UTF-8 byte.
func URLDecode(str cty.Value) (cty.Value, error) {
return URLDecodeFunc.Call([]cty.Value{str})
}
// TextEncodeBase64 applies Base64 encoding to a string that was encoded before with a target encoding.
//
// OpenTofu uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
//
// First step is to apply the target IANA encoding (e.g. UTF-16LE).
// Strings in the OpenTofu language are sequences of unicode characters rather
// than bytes, so this function will first encode the characters from the string
// as UTF-8, and then apply Base64 encoding to the result.
func TextEncodeBase64(str, enc cty.Value) (cty.Value, error) {
return TextEncodeBase64Func.Call([]cty.Value{str, enc})
}
// TextDecodeBase64 decodes a string containing a base64 sequence whereas a specific encoding of the string is expected.
//
// OpenTofu uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
//
// Strings in the OpenTofu language are sequences of unicode characters rather
// than bytes, so this function will also interpret the resulting bytes as
// the target encoding.
func TextDecodeBase64(str, enc cty.Value) (cty.Value, error) {
return TextDecodeBase64Func.Call([]cty.Value{str, enc})
}