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
|
package yaml
import (
"encoding/base64"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/zclconf/go-cty/cty"
)
type resolveMapItem struct {
value cty.Value
tag string
}
var resolveTable = make([]byte, 256)
var resolveMap = make(map[string]resolveMapItem)
// Numeric literal regular expressions from the YAML 1.2 spec:
//
// https://yaml.org/spec/1.2/spec.html#id2805071
var integerLiteralRegexp = regexp.MustCompile(`` +
// start of string, optional sign, and one of:
`\A[-+]?(` +
// octal literal with 0o prefix and optional _ spaces
`|0o[0-7_]+` +
// decimal literal and optional _ spaces
`|[0-9_]+` +
// hexadecimal literal with 0x prefix and optional _ spaces
`|0x[0-9a-fA-F_]+` +
// end of group, and end of string
`)\z`,
)
var floatLiteralRegexp = regexp.MustCompile(
`\A[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?\z`,
)
func init() {
t := resolveTable
t[int('+')] = 'S' // Sign
t[int('-')] = 'S'
for _, c := range "0123456789" {
t[int(c)] = 'D' // Digit
}
for _, c := range "yYnNtTfFoO~" {
t[int(c)] = 'M' // In map
}
t[int('.')] = '.' // Float (potentially in map)
var resolveMapList = []struct {
v cty.Value
tag string
l []string
}{
{cty.True, yaml_BOOL_TAG, []string{"y", "Y", "yes", "Yes", "YES"}},
{cty.True, yaml_BOOL_TAG, []string{"true", "True", "TRUE"}},
{cty.True, yaml_BOOL_TAG, []string{"on", "On", "ON"}},
{cty.False, yaml_BOOL_TAG, []string{"n", "N", "no", "No", "NO"}},
{cty.False, yaml_BOOL_TAG, []string{"false", "False", "FALSE"}},
{cty.False, yaml_BOOL_TAG, []string{"off", "Off", "OFF"}},
{cty.NullVal(cty.DynamicPseudoType), yaml_NULL_TAG, []string{"", "~", "null", "Null", "NULL"}},
{cty.PositiveInfinity, yaml_FLOAT_TAG, []string{".inf", ".Inf", ".INF"}},
{cty.PositiveInfinity, yaml_FLOAT_TAG, []string{"+.inf", "+.Inf", "+.INF"}},
{cty.NegativeInfinity, yaml_FLOAT_TAG, []string{"-.inf", "-.Inf", "-.INF"}},
}
m := resolveMap
for _, item := range resolveMapList {
for _, s := range item.l {
m[s] = resolveMapItem{item.v, item.tag}
}
}
}
const longTagPrefix = "tag:yaml.org,2002:"
func shortTag(tag string) string {
// TODO This can easily be made faster and produce less garbage.
if strings.HasPrefix(tag, longTagPrefix) {
return "!!" + tag[len(longTagPrefix):]
}
return tag
}
func longTag(tag string) string {
if strings.HasPrefix(tag, "!!") {
return longTagPrefix + tag[2:]
}
return tag
}
func resolvableTag(tag string) bool {
switch tag {
case "", yaml_STR_TAG, yaml_BOOL_TAG, yaml_INT_TAG, yaml_FLOAT_TAG, yaml_NULL_TAG, yaml_TIMESTAMP_TAG, yaml_BINARY_TAG:
return true
}
return false
}
var yamlStyleFloat = regexp.MustCompile(`^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$`)
func (c *Converter) resolveScalar(tag string, src string, style yaml_scalar_style_t) (cty.Value, error) {
if !resolvableTag(tag) {
return cty.NilVal, fmt.Errorf("unsupported tag %q", tag)
}
// Any data is accepted as a !!str or !!binary.
// Otherwise, the prefix is enough of a hint about what it might be.
hint := byte('N')
if src != "" {
hint = resolveTable[src[0]]
}
if hint != 0 && tag != yaml_STR_TAG && tag != yaml_BINARY_TAG {
if style == yaml_SINGLE_QUOTED_SCALAR_STYLE || style == yaml_DOUBLE_QUOTED_SCALAR_STYLE {
return cty.StringVal(src), nil
}
// Handle things we can lookup in a map.
if item, ok := resolveMap[src]; ok {
return item.value, nil
}
if tag == "" {
for _, nan := range []string{".nan", ".NaN", ".NAN"} {
if src == nan {
// cty cannot represent NaN, so this is an error
return cty.NilVal, fmt.Errorf("floating point NaN is not supported")
}
}
}
// Base 60 floats are intentionally not supported.
switch hint {
case 'M':
// We've already checked the map above.
case '.':
// Not in the map, so maybe a normal float.
if numberVal, err := cty.ParseNumberVal(src); err == nil {
return numberVal, nil
}
case 'D', 'S':
// Int, float, or timestamp.
// Only try values as a timestamp if the value is unquoted or there's an explicit
// !!timestamp tag.
if tag == "" || tag == yaml_TIMESTAMP_TAG {
t, ok := parseTimestamp(src)
if ok {
// cty has no timestamp type, but its functions stdlib
// conventionally uses strings in an RFC3339 encoding
// to represent time, so we'll follow that convention here.
return cty.StringVal(t.Format(time.RFC3339)), nil
}
}
if integerLiteralRegexp.MatchString(src) {
tag = yaml_INT_TAG // will handle parsing below in our tag switch
break
}
if floatLiteralRegexp.MatchString(src) {
tag = yaml_FLOAT_TAG // will handle parsing below in our tag switch
break
}
default:
panic(fmt.Sprintf("cannot resolve tag %q with source %q", tag, src))
}
}
if tag == "" && src == "<<" {
return mergeMappingVal, nil
}
switch tag {
case yaml_STR_TAG, yaml_BINARY_TAG:
// If it's binary then we want to keep the base64 representation, because
// cty has no binary type, but we will check that it's actually base64.
if tag == yaml_BINARY_TAG {
_, err := base64.StdEncoding.DecodeString(src)
if err != nil {
return cty.NilVal, fmt.Errorf("cannot parse %q as %s: not valid base64", src, tag)
}
}
return cty.StringVal(src), nil
case yaml_BOOL_TAG:
item, ok := resolveMap[src]
if !ok || item.tag != yaml_BOOL_TAG {
return cty.NilVal, fmt.Errorf("cannot parse %q as %s", src, tag)
}
return item.value, nil
case yaml_FLOAT_TAG, yaml_INT_TAG:
// Note: We don't actually check that a value tagged INT is a whole
// number here. We could, but cty generally doesn't care about the
// int/float distinction, so we'll just be generous and accept it.
plain := strings.Replace(src, "_", "", -1)
if numberVal, err := cty.ParseNumberVal(plain); err == nil { // handles decimal integers and floats
return numberVal, nil
}
if intv, err := strconv.ParseInt(plain, 0, 64); err == nil { // handles 0x and 00 prefixes
return cty.NumberIntVal(intv), nil
}
if uintv, err := strconv.ParseUint(plain, 0, 64); err == nil { // handles 0x and 00 prefixes
return cty.NumberUIntVal(uintv), nil
}
return cty.NilVal, fmt.Errorf("cannot parse %q as %s", src, tag)
case yaml_TIMESTAMP_TAG:
t, ok := parseTimestamp(src)
if ok {
// cty has no timestamp type, but its functions stdlib
// conventionally uses strings in an RFC3339 encoding
// to represent time, so we'll follow that convention here.
return cty.StringVal(t.Format(time.RFC3339)), nil
}
return cty.NilVal, fmt.Errorf("cannot parse %q as %s", src, tag)
case yaml_NULL_TAG:
return cty.NullVal(cty.DynamicPseudoType), nil
case "":
return cty.StringVal(src), nil
default:
return cty.NilVal, fmt.Errorf("unsupported tag %q", tag)
}
}
// encodeBase64 encodes s as base64 that is broken up into multiple lines
// as appropriate for the resulting length.
func encodeBase64(s string) string {
const lineLen = 70
encLen := base64.StdEncoding.EncodedLen(len(s))
lines := encLen/lineLen + 1
buf := make([]byte, encLen*2+lines)
in := buf[0:encLen]
out := buf[encLen:]
base64.StdEncoding.Encode(in, []byte(s))
k := 0
for i := 0; i < len(in); i += lineLen {
j := i + lineLen
if j > len(in) {
j = len(in)
}
k += copy(out[k:], in[i:j])
if lines > 1 {
out[k] = '\n'
k++
}
}
return string(out[:k])
}
// This is a subset of the formats allowed by the regular expression
// defined at http://yaml.org/type/timestamp.html.
var allowedTimestampFormats = []string{
"2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields.
"2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t".
"2006-1-2 15:4:5.999999999", // space separated with no time zone
"2006-1-2", // date only
// Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5"
// from the set of examples.
}
// parseTimestamp parses s as a timestamp string and
// returns the timestamp and reports whether it succeeded.
// Timestamp formats are defined at http://yaml.org/type/timestamp.html
func parseTimestamp(s string) (time.Time, bool) {
// TODO write code to check all the formats supported by
// http://yaml.org/type/timestamp.html instead of using time.Parse.
// Quick check: all date formats start with YYYY-.
i := 0
for ; i < len(s); i++ {
if c := s[i]; c < '0' || c > '9' {
break
}
}
if i != 4 || i == len(s) || s[i] != '-' {
return time.Time{}, false
}
for _, format := range allowedTimestampFormats {
if t, err := time.Parse(format, s); err == nil {
return t, true
}
}
return time.Time{}, false
}
type mergeMapping struct{}
var mergeMappingTy = cty.Capsule("merge mapping", reflect.TypeOf(mergeMapping{}))
var mergeMappingVal = cty.CapsuleVal(mergeMappingTy, &mergeMapping{})
|