1package toml
2
3import (
4	"bytes"
5	"fmt"
6	"io"
7	"reflect"
8	"sort"
9	"strconv"
10	"strings"
11	"time"
12)
13
14// encodes a string to a TOML-compliant string value
15func encodeTomlString(value string) string {
16	result := ""
17	for _, rr := range value {
18		switch rr {
19		case '\b':
20			result += "\\b"
21		case '\t':
22			result += "\\t"
23		case '\n':
24			result += "\\n"
25		case '\f':
26			result += "\\f"
27		case '\r':
28			result += "\\r"
29		case '"':
30			result += "\\\""
31		case '\\':
32			result += "\\\\"
33		default:
34			intRr := uint16(rr)
35			if intRr < 0x001F {
36				result += fmt.Sprintf("\\u%0.4X", intRr)
37			} else {
38				result += string(rr)
39			}
40		}
41	}
42	return result
43}
44
45func tomlValueStringRepresentation(v interface{}) (string, error) {
46	switch value := v.(type) {
47	case uint64:
48		return strconv.FormatUint(value, 10), nil
49	case int64:
50		return strconv.FormatInt(value, 10), nil
51	case float64:
52		return strconv.FormatFloat(value, 'f', -1, 32), nil
53	case string:
54		return "\"" + encodeTomlString(value) + "\"", nil
55	case []byte:
56		b, _ := v.([]byte)
57		return tomlValueStringRepresentation(string(b))
58	case bool:
59		if value {
60			return "true", nil
61		}
62		return "false", nil
63	case time.Time:
64		return value.Format(time.RFC3339), nil
65	case nil:
66		return "", nil
67	}
68
69	rv := reflect.ValueOf(v)
70
71	if rv.Kind() == reflect.Slice {
72		values := []string{}
73		for i := 0; i < rv.Len(); i++ {
74			item := rv.Index(i).Interface()
75			itemRepr, err := tomlValueStringRepresentation(item)
76			if err != nil {
77				return "", err
78			}
79			values = append(values, itemRepr)
80		}
81		return "[" + strings.Join(values, ",") + "]", nil
82	}
83	return "", fmt.Errorf("unsupported value type %T: %v", v, v)
84}
85
86func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64) (int64, error) {
87	simpleValuesKeys := make([]string, 0)
88	complexValuesKeys := make([]string, 0)
89
90	for k := range t.values {
91		v := t.values[k]
92		switch v.(type) {
93		case *Tree, []*Tree:
94			complexValuesKeys = append(complexValuesKeys, k)
95		default:
96			simpleValuesKeys = append(simpleValuesKeys, k)
97		}
98	}
99
100	sort.Strings(simpleValuesKeys)
101	sort.Strings(complexValuesKeys)
102
103	for _, k := range simpleValuesKeys {
104		v, ok := t.values[k].(*tomlValue)
105		if !ok {
106			return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
107		}
108
109		repr, err := tomlValueStringRepresentation(v.value)
110		if err != nil {
111			return bytesCount, err
112		}
113
114		kvRepr := fmt.Sprintf("%s%s = %s\n", indent, k, repr)
115		writtenBytesCount, err := w.Write([]byte(kvRepr))
116		bytesCount += int64(writtenBytesCount)
117		if err != nil {
118			return bytesCount, err
119		}
120	}
121
122	for _, k := range complexValuesKeys {
123		v := t.values[k]
124
125		combinedKey := k
126		if keyspace != "" {
127			combinedKey = keyspace + "." + combinedKey
128		}
129
130		switch node := v.(type) {
131		// node has to be of those two types given how keys are sorted above
132		case *Tree:
133			tableName := fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
134			writtenBytesCount, err := w.Write([]byte(tableName))
135			bytesCount += int64(writtenBytesCount)
136			if err != nil {
137				return bytesCount, err
138			}
139			bytesCount, err = node.writeTo(w, indent+"  ", combinedKey, bytesCount)
140			if err != nil {
141				return bytesCount, err
142			}
143		case []*Tree:
144			for _, subTree := range node {
145				tableArrayName := fmt.Sprintf("\n%s[[%s]]\n", indent, combinedKey)
146				writtenBytesCount, err := w.Write([]byte(tableArrayName))
147				bytesCount += int64(writtenBytesCount)
148				if err != nil {
149					return bytesCount, err
150				}
151
152				bytesCount, err = subTree.writeTo(w, indent+"  ", combinedKey, bytesCount)
153				if err != nil {
154					return bytesCount, err
155				}
156			}
157		}
158	}
159
160	return bytesCount, nil
161}
162
163// WriteTo encode the Tree as Toml and writes it to the writer w.
164// Returns the number of bytes written in case of success, or an error if anything happened.
165func (t *Tree) WriteTo(w io.Writer) (int64, error) {
166	return t.writeTo(w, "", "", 0)
167}
168
169// ToTomlString generates a human-readable representation of the current tree.
170// Output spans multiple lines, and is suitable for ingest by a TOML parser.
171// If the conversion cannot be performed, ToString returns a non-nil error.
172func (t *Tree) ToTomlString() (string, error) {
173	var buf bytes.Buffer
174	_, err := t.WriteTo(&buf)
175	if err != nil {
176		return "", err
177	}
178	return buf.String(), nil
179}
180
181// String generates a human-readable representation of the current tree.
182// Alias of ToString. Present to implement the fmt.Stringer interface.
183func (t *Tree) String() string {
184	result, _ := t.ToTomlString()
185	return result
186}
187
188// ToMap recursively generates a representation of the tree using Go built-in structures.
189// The following types are used:
190//
191//	* bool
192//	* float64
193//	* int64
194//	* string
195//	* uint64
196//	* time.Time
197//	* map[string]interface{} (where interface{} is any of this list)
198//	* []interface{} (where interface{} is any of this list)
199func (t *Tree) ToMap() map[string]interface{} {
200	result := map[string]interface{}{}
201
202	for k, v := range t.values {
203		switch node := v.(type) {
204		case []*Tree:
205			var array []interface{}
206			for _, item := range node {
207				array = append(array, item.ToMap())
208			}
209			result[k] = array
210		case *Tree:
211			result[k] = node.ToMap()
212		case *tomlValue:
213			result[k] = node.value
214		}
215	}
216	return result
217}
218