vendor/github.com/pelletier/go-toml/tomltree_write.go
changeset 265 05c40b36d3b2
parent 264 8f478162d991
child 266 80973a656b81
equal deleted inserted replaced
264:8f478162d991 265:05c40b36d3b2
     1 package toml
       
     2 
       
     3 import (
       
     4 	"bytes"
       
     5 	"fmt"
       
     6 	"io"
       
     7 	"math"
       
     8 	"math/big"
       
     9 	"reflect"
       
    10 	"sort"
       
    11 	"strconv"
       
    12 	"strings"
       
    13 	"time"
       
    14 )
       
    15 
       
    16 type valueComplexity int
       
    17 
       
    18 const (
       
    19 	valueSimple valueComplexity = iota + 1
       
    20 	valueComplex
       
    21 )
       
    22 
       
    23 type sortNode struct {
       
    24 	key        string
       
    25 	complexity valueComplexity
       
    26 }
       
    27 
       
    28 // Encodes a string to a TOML-compliant multi-line string value
       
    29 // This function is a clone of the existing encodeTomlString function, except that whitespace characters
       
    30 // are preserved. Quotation marks and backslashes are also not escaped.
       
    31 func encodeMultilineTomlString(value string, commented string) string {
       
    32 	var b bytes.Buffer
       
    33 	adjacentQuoteCount := 0
       
    34 
       
    35 	b.WriteString(commented)
       
    36 	for i, rr := range value {
       
    37 		if rr != '"' {
       
    38 			adjacentQuoteCount = 0
       
    39 		} else {
       
    40 			adjacentQuoteCount++
       
    41 		}
       
    42 		switch rr {
       
    43 		case '\b':
       
    44 			b.WriteString(`\b`)
       
    45 		case '\t':
       
    46 			b.WriteString("\t")
       
    47 		case '\n':
       
    48 			b.WriteString("\n" + commented)
       
    49 		case '\f':
       
    50 			b.WriteString(`\f`)
       
    51 		case '\r':
       
    52 			b.WriteString("\r")
       
    53 		case '"':
       
    54 			if adjacentQuoteCount >= 3 || i == len(value)-1 {
       
    55 				adjacentQuoteCount = 0
       
    56 				b.WriteString(`\"`)
       
    57 			} else {
       
    58 				b.WriteString(`"`)
       
    59 			}
       
    60 		case '\\':
       
    61 			b.WriteString(`\`)
       
    62 		default:
       
    63 			intRr := uint16(rr)
       
    64 			if intRr < 0x001F {
       
    65 				b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
       
    66 			} else {
       
    67 				b.WriteRune(rr)
       
    68 			}
       
    69 		}
       
    70 	}
       
    71 	return b.String()
       
    72 }
       
    73 
       
    74 // Encodes a string to a TOML-compliant string value
       
    75 func encodeTomlString(value string) string {
       
    76 	var b bytes.Buffer
       
    77 
       
    78 	for _, rr := range value {
       
    79 		switch rr {
       
    80 		case '\b':
       
    81 			b.WriteString(`\b`)
       
    82 		case '\t':
       
    83 			b.WriteString(`\t`)
       
    84 		case '\n':
       
    85 			b.WriteString(`\n`)
       
    86 		case '\f':
       
    87 			b.WriteString(`\f`)
       
    88 		case '\r':
       
    89 			b.WriteString(`\r`)
       
    90 		case '"':
       
    91 			b.WriteString(`\"`)
       
    92 		case '\\':
       
    93 			b.WriteString(`\\`)
       
    94 		default:
       
    95 			intRr := uint16(rr)
       
    96 			if intRr < 0x001F {
       
    97 				b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
       
    98 			} else {
       
    99 				b.WriteRune(rr)
       
   100 			}
       
   101 		}
       
   102 	}
       
   103 	return b.String()
       
   104 }
       
   105 
       
   106 func tomlTreeStringRepresentation(t *Tree, ord MarshalOrder) (string, error) {
       
   107 	var orderedVals []sortNode
       
   108 	switch ord {
       
   109 	case OrderPreserve:
       
   110 		orderedVals = sortByLines(t)
       
   111 	default:
       
   112 		orderedVals = sortAlphabetical(t)
       
   113 	}
       
   114 
       
   115 	var values []string
       
   116 	for _, node := range orderedVals {
       
   117 		k := node.key
       
   118 		v := t.values[k]
       
   119 
       
   120 		repr, err := tomlValueStringRepresentation(v, "", "", ord, false)
       
   121 		if err != nil {
       
   122 			return "", err
       
   123 		}
       
   124 		values = append(values, quoteKeyIfNeeded(k)+" = "+repr)
       
   125 	}
       
   126 	return "{ " + strings.Join(values, ", ") + " }", nil
       
   127 }
       
   128 
       
   129 func tomlValueStringRepresentation(v interface{}, commented string, indent string, ord MarshalOrder, arraysOneElementPerLine bool) (string, error) {
       
   130 	// this interface check is added to dereference the change made in the writeTo function.
       
   131 	// That change was made to allow this function to see formatting options.
       
   132 	tv, ok := v.(*tomlValue)
       
   133 	if ok {
       
   134 		v = tv.value
       
   135 	} else {
       
   136 		tv = &tomlValue{}
       
   137 	}
       
   138 
       
   139 	switch value := v.(type) {
       
   140 	case uint64:
       
   141 		return strconv.FormatUint(value, 10), nil
       
   142 	case int64:
       
   143 		return strconv.FormatInt(value, 10), nil
       
   144 	case float64:
       
   145 		// Default bit length is full 64
       
   146 		bits := 64
       
   147 		// Float panics if nan is used
       
   148 		if !math.IsNaN(value) {
       
   149 			// if 32 bit accuracy is enough to exactly show, use 32
       
   150 			_, acc := big.NewFloat(value).Float32()
       
   151 			if acc == big.Exact {
       
   152 				bits = 32
       
   153 			}
       
   154 		}
       
   155 		if math.Trunc(value) == value {
       
   156 			return strings.ToLower(strconv.FormatFloat(value, 'f', 1, bits)), nil
       
   157 		}
       
   158 		return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil
       
   159 	case string:
       
   160 		if tv.multiline {
       
   161 			if tv.literal {
       
   162 				b := strings.Builder{}
       
   163 				b.WriteString("'''\n")
       
   164 				b.Write([]byte(value))
       
   165 				b.WriteString("\n'''")
       
   166 				return b.String(), nil
       
   167 			} else {
       
   168 				return "\"\"\"\n" + encodeMultilineTomlString(value, commented) + "\"\"\"", nil
       
   169 			}
       
   170 		}
       
   171 		return "\"" + encodeTomlString(value) + "\"", nil
       
   172 	case []byte:
       
   173 		b, _ := v.([]byte)
       
   174 		return string(b), nil
       
   175 	case bool:
       
   176 		if value {
       
   177 			return "true", nil
       
   178 		}
       
   179 		return "false", nil
       
   180 	case time.Time:
       
   181 		return value.Format(time.RFC3339), nil
       
   182 	case LocalDate:
       
   183 		return value.String(), nil
       
   184 	case LocalDateTime:
       
   185 		return value.String(), nil
       
   186 	case LocalTime:
       
   187 		return value.String(), nil
       
   188 	case *Tree:
       
   189 		return tomlTreeStringRepresentation(value, ord)
       
   190 	case nil:
       
   191 		return "", nil
       
   192 	}
       
   193 
       
   194 	rv := reflect.ValueOf(v)
       
   195 
       
   196 	if rv.Kind() == reflect.Slice {
       
   197 		var values []string
       
   198 		for i := 0; i < rv.Len(); i++ {
       
   199 			item := rv.Index(i).Interface()
       
   200 			itemRepr, err := tomlValueStringRepresentation(item, commented, indent, ord, arraysOneElementPerLine)
       
   201 			if err != nil {
       
   202 				return "", err
       
   203 			}
       
   204 			values = append(values, itemRepr)
       
   205 		}
       
   206 		if arraysOneElementPerLine && len(values) > 1 {
       
   207 			stringBuffer := bytes.Buffer{}
       
   208 			valueIndent := indent + `  ` // TODO: move that to a shared encoder state
       
   209 
       
   210 			stringBuffer.WriteString("[\n")
       
   211 
       
   212 			for _, value := range values {
       
   213 				stringBuffer.WriteString(valueIndent)
       
   214 				stringBuffer.WriteString(commented + value)
       
   215 				stringBuffer.WriteString(`,`)
       
   216 				stringBuffer.WriteString("\n")
       
   217 			}
       
   218 
       
   219 			stringBuffer.WriteString(indent + commented + "]")
       
   220 
       
   221 			return stringBuffer.String(), nil
       
   222 		}
       
   223 		return "[" + strings.Join(values, ", ") + "]", nil
       
   224 	}
       
   225 	return "", fmt.Errorf("unsupported value type %T: %v", v, v)
       
   226 }
       
   227 
       
   228 func getTreeArrayLine(trees []*Tree) (line int) {
       
   229 	// Prevent returning 0 for empty trees
       
   230 	line = int(^uint(0) >> 1)
       
   231 	// get lowest line number >= 0
       
   232 	for _, tv := range trees {
       
   233 		if tv.position.Line < line || line == 0 {
       
   234 			line = tv.position.Line
       
   235 		}
       
   236 	}
       
   237 	return
       
   238 }
       
   239 
       
   240 func sortByLines(t *Tree) (vals []sortNode) {
       
   241 	var (
       
   242 		line  int
       
   243 		lines []int
       
   244 		tv    *Tree
       
   245 		tom   *tomlValue
       
   246 		node  sortNode
       
   247 	)
       
   248 	vals = make([]sortNode, 0)
       
   249 	m := make(map[int]sortNode)
       
   250 
       
   251 	for k := range t.values {
       
   252 		v := t.values[k]
       
   253 		switch v.(type) {
       
   254 		case *Tree:
       
   255 			tv = v.(*Tree)
       
   256 			line = tv.position.Line
       
   257 			node = sortNode{key: k, complexity: valueComplex}
       
   258 		case []*Tree:
       
   259 			line = getTreeArrayLine(v.([]*Tree))
       
   260 			node = sortNode{key: k, complexity: valueComplex}
       
   261 		default:
       
   262 			tom = v.(*tomlValue)
       
   263 			line = tom.position.Line
       
   264 			node = sortNode{key: k, complexity: valueSimple}
       
   265 		}
       
   266 		lines = append(lines, line)
       
   267 		vals = append(vals, node)
       
   268 		m[line] = node
       
   269 	}
       
   270 	sort.Ints(lines)
       
   271 
       
   272 	for i, line := range lines {
       
   273 		vals[i] = m[line]
       
   274 	}
       
   275 
       
   276 	return vals
       
   277 }
       
   278 
       
   279 func sortAlphabetical(t *Tree) (vals []sortNode) {
       
   280 	var (
       
   281 		node     sortNode
       
   282 		simpVals []string
       
   283 		compVals []string
       
   284 	)
       
   285 	vals = make([]sortNode, 0)
       
   286 	m := make(map[string]sortNode)
       
   287 
       
   288 	for k := range t.values {
       
   289 		v := t.values[k]
       
   290 		switch v.(type) {
       
   291 		case *Tree, []*Tree:
       
   292 			node = sortNode{key: k, complexity: valueComplex}
       
   293 			compVals = append(compVals, node.key)
       
   294 		default:
       
   295 			node = sortNode{key: k, complexity: valueSimple}
       
   296 			simpVals = append(simpVals, node.key)
       
   297 		}
       
   298 		vals = append(vals, node)
       
   299 		m[node.key] = node
       
   300 	}
       
   301 
       
   302 	// Simples first to match previous implementation
       
   303 	sort.Strings(simpVals)
       
   304 	i := 0
       
   305 	for _, key := range simpVals {
       
   306 		vals[i] = m[key]
       
   307 		i++
       
   308 	}
       
   309 
       
   310 	sort.Strings(compVals)
       
   311 	for _, key := range compVals {
       
   312 		vals[i] = m[key]
       
   313 		i++
       
   314 	}
       
   315 
       
   316 	return vals
       
   317 }
       
   318 
       
   319 func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) {
       
   320 	return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical, "  ", false, false)
       
   321 }
       
   322 
       
   323 func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord MarshalOrder, indentString string, compactComments, parentCommented bool) (int64, error) {
       
   324 	var orderedVals []sortNode
       
   325 
       
   326 	switch ord {
       
   327 	case OrderPreserve:
       
   328 		orderedVals = sortByLines(t)
       
   329 	default:
       
   330 		orderedVals = sortAlphabetical(t)
       
   331 	}
       
   332 
       
   333 	for _, node := range orderedVals {
       
   334 		switch node.complexity {
       
   335 		case valueComplex:
       
   336 			k := node.key
       
   337 			v := t.values[k]
       
   338 
       
   339 			combinedKey := quoteKeyIfNeeded(k)
       
   340 			if keyspace != "" {
       
   341 				combinedKey = keyspace + "." + combinedKey
       
   342 			}
       
   343 
       
   344 			switch node := v.(type) {
       
   345 			// node has to be of those two types given how keys are sorted above
       
   346 			case *Tree:
       
   347 				tv, ok := t.values[k].(*Tree)
       
   348 				if !ok {
       
   349 					return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
       
   350 				}
       
   351 				if tv.comment != "" {
       
   352 					comment := strings.Replace(tv.comment, "\n", "\n"+indent+"#", -1)
       
   353 					start := "# "
       
   354 					if strings.HasPrefix(comment, "#") {
       
   355 						start = ""
       
   356 					}
       
   357 					writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment)
       
   358 					bytesCount += int64(writtenBytesCountComment)
       
   359 					if errc != nil {
       
   360 						return bytesCount, errc
       
   361 					}
       
   362 				}
       
   363 
       
   364 				var commented string
       
   365 				if parentCommented || t.commented || tv.commented {
       
   366 					commented = "# "
       
   367 				}
       
   368 				writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n")
       
   369 				bytesCount += int64(writtenBytesCount)
       
   370 				if err != nil {
       
   371 					return bytesCount, err
       
   372 				}
       
   373 				bytesCount, err = node.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || tv.commented)
       
   374 				if err != nil {
       
   375 					return bytesCount, err
       
   376 				}
       
   377 			case []*Tree:
       
   378 				for _, subTree := range node {
       
   379 					var commented string
       
   380 					if parentCommented || t.commented || subTree.commented {
       
   381 						commented = "# "
       
   382 					}
       
   383 					writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n")
       
   384 					bytesCount += int64(writtenBytesCount)
       
   385 					if err != nil {
       
   386 						return bytesCount, err
       
   387 					}
       
   388 
       
   389 					bytesCount, err = subTree.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || subTree.commented)
       
   390 					if err != nil {
       
   391 						return bytesCount, err
       
   392 					}
       
   393 				}
       
   394 			}
       
   395 		default: // Simple
       
   396 			k := node.key
       
   397 			v, ok := t.values[k].(*tomlValue)
       
   398 			if !ok {
       
   399 				return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
       
   400 			}
       
   401 
       
   402 			var commented string
       
   403 			if parentCommented || t.commented || v.commented {
       
   404 				commented = "# "
       
   405 			}
       
   406 			repr, err := tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine)
       
   407 			if err != nil {
       
   408 				return bytesCount, err
       
   409 			}
       
   410 
       
   411 			if v.comment != "" {
       
   412 				comment := strings.Replace(v.comment, "\n", "\n"+indent+"#", -1)
       
   413 				start := "# "
       
   414 				if strings.HasPrefix(comment, "#") {
       
   415 					start = ""
       
   416 				}
       
   417 				if !compactComments {
       
   418 					writtenBytesCountComment, errc := writeStrings(w, "\n")
       
   419 					bytesCount += int64(writtenBytesCountComment)
       
   420 					if errc != nil {
       
   421 						return bytesCount, errc
       
   422 					}
       
   423 				}
       
   424 				writtenBytesCountComment, errc := writeStrings(w, indent, start, comment, "\n")
       
   425 				bytesCount += int64(writtenBytesCountComment)
       
   426 				if errc != nil {
       
   427 					return bytesCount, errc
       
   428 				}
       
   429 			}
       
   430 
       
   431 			quotedKey := quoteKeyIfNeeded(k)
       
   432 			writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n")
       
   433 			bytesCount += int64(writtenBytesCount)
       
   434 			if err != nil {
       
   435 				return bytesCount, err
       
   436 			}
       
   437 		}
       
   438 	}
       
   439 
       
   440 	return bytesCount, nil
       
   441 }
       
   442 
       
   443 // quote a key if it does not fit the bare key format (A-Za-z0-9_-)
       
   444 // quoted keys use the same rules as strings
       
   445 func quoteKeyIfNeeded(k string) string {
       
   446 	// when encoding a map with the 'quoteMapKeys' option enabled, the tree will contain
       
   447 	// keys that have already been quoted.
       
   448 	// not an ideal situation, but good enough of a stop gap.
       
   449 	if len(k) >= 2 && k[0] == '"' && k[len(k)-1] == '"' {
       
   450 		return k
       
   451 	}
       
   452 	isBare := true
       
   453 	for _, r := range k {
       
   454 		if !isValidBareChar(r) {
       
   455 			isBare = false
       
   456 			break
       
   457 		}
       
   458 	}
       
   459 	if isBare {
       
   460 		return k
       
   461 	}
       
   462 	return quoteKey(k)
       
   463 }
       
   464 
       
   465 func quoteKey(k string) string {
       
   466 	return "\"" + encodeTomlString(k) + "\""
       
   467 }
       
   468 
       
   469 func writeStrings(w io.Writer, s ...string) (int, error) {
       
   470 	var n int
       
   471 	for i := range s {
       
   472 		b, err := io.WriteString(w, s[i])
       
   473 		n += b
       
   474 		if err != nil {
       
   475 			return n, err
       
   476 		}
       
   477 	}
       
   478 	return n, nil
       
   479 }
       
   480 
       
   481 // WriteTo encode the Tree as Toml and writes it to the writer w.
       
   482 // Returns the number of bytes written in case of success, or an error if anything happened.
       
   483 func (t *Tree) WriteTo(w io.Writer) (int64, error) {
       
   484 	return t.writeTo(w, "", "", 0, false)
       
   485 }
       
   486 
       
   487 // ToTomlString generates a human-readable representation of the current tree.
       
   488 // Output spans multiple lines, and is suitable for ingest by a TOML parser.
       
   489 // If the conversion cannot be performed, ToString returns a non-nil error.
       
   490 func (t *Tree) ToTomlString() (string, error) {
       
   491 	b, err := t.Marshal()
       
   492 	if err != nil {
       
   493 		return "", err
       
   494 	}
       
   495 	return string(b), nil
       
   496 }
       
   497 
       
   498 // String generates a human-readable representation of the current tree.
       
   499 // Alias of ToString. Present to implement the fmt.Stringer interface.
       
   500 func (t *Tree) String() string {
       
   501 	result, _ := t.ToTomlString()
       
   502 	return result
       
   503 }
       
   504 
       
   505 // ToMap recursively generates a representation of the tree using Go built-in structures.
       
   506 // The following types are used:
       
   507 //
       
   508 //	* bool
       
   509 //	* float64
       
   510 //	* int64
       
   511 //	* string
       
   512 //	* uint64
       
   513 //	* time.Time
       
   514 //	* map[string]interface{} (where interface{} is any of this list)
       
   515 //	* []interface{} (where interface{} is any of this list)
       
   516 func (t *Tree) ToMap() map[string]interface{} {
       
   517 	result := map[string]interface{}{}
       
   518 
       
   519 	for k, v := range t.values {
       
   520 		switch node := v.(type) {
       
   521 		case []*Tree:
       
   522 			var array []interface{}
       
   523 			for _, item := range node {
       
   524 				array = append(array, item.ToMap())
       
   525 			}
       
   526 			result[k] = array
       
   527 		case *Tree:
       
   528 			result[k] = node.ToMap()
       
   529 		case *tomlValue:
       
   530 			result[k] = tomlValueToGo(node.value)
       
   531 		}
       
   532 	}
       
   533 	return result
       
   534 }
       
   535 
       
   536 func tomlValueToGo(v interface{}) interface{} {
       
   537 	if tree, ok := v.(*Tree); ok {
       
   538 		return tree.ToMap()
       
   539 	}
       
   540 
       
   541 	rv := reflect.ValueOf(v)
       
   542 
       
   543 	if rv.Kind() != reflect.Slice {
       
   544 		return v
       
   545 	}
       
   546 	values := make([]interface{}, rv.Len())
       
   547 	for i := 0; i < rv.Len(); i++ {
       
   548 		item := rv.Index(i).Interface()
       
   549 		values[i] = tomlValueToGo(item)
       
   550 	}
       
   551 	return values
       
   552 }