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