package toml import ( "bufio" "encoding" "errors" "fmt" "io" "math" "reflect" "sort" "strconv" "strings" "time" "github.com/BurntSushi/toml/internal" ) type tomlEncodeError struct{ error } var ( errArrayNilElement = errors.New("toml: cannot encode array with nil element") errNonString = errors.New("toml: cannot encode a map with non-string key type") errNoKey = errors.New("toml: top-level values must be Go maps or structs") errAnything = errors.New("") // used in testing ) var dblQuotedReplacer = strings.NewReplacer( "\"", "\\\"", "\\", "\\\\", "\x00", `\u0000`, "\x01", `\u0001`, "\x02", `\u0002`, "\x03", `\u0003`, "\x04", `\u0004`, "\x05", `\u0005`, "\x06", `\u0006`, "\x07", `\u0007`, "\b", `\b`, "\t", `\t`, "\n", `\n`, "\x0b", `\u000b`, "\f", `\f`, "\r", `\r`, "\x0e", `\u000e`, "\x0f", `\u000f`, "\x10", `\u0010`, "\x11", `\u0011`, "\x12", `\u0012`, "\x13", `\u0013`, "\x14", `\u0014`, "\x15", `\u0015`, "\x16", `\u0016`, "\x17", `\u0017`, "\x18", `\u0018`, "\x19", `\u0019`, "\x1a", `\u001a`, "\x1b", `\u001b`, "\x1c", `\u001c`, "\x1d", `\u001d`, "\x1e", `\u001e`, "\x1f", `\u001f`, "\x7f", `\u007f`, ) // Marshaler is the interface implemented by types that can marshal themselves // into valid TOML. type Marshaler interface { MarshalTOML() ([]byte, error) } // Encoder encodes a Go to a TOML document. // // The mapping between Go values and TOML values should be precisely the same as // for the Decode* functions. // // The toml.Marshaler and encoder.TextMarshaler interfaces are supported to // encoding the value as custom TOML. // // If you want to write arbitrary binary data then you will need to use // something like base64 since TOML does not have any binary types. // // When encoding TOML hashes (Go maps or structs), keys without any sub-hashes // are encoded first. // // Go maps will be sorted alphabetically by key for deterministic output. // // Encoding Go values without a corresponding TOML representation will return an // error. Examples of this includes maps with non-string keys, slices with nil // elements, embedded non-struct types, and nested slices containing maps or // structs. (e.g. [][]map[string]string is not allowed but []map[string]string // is okay, as is []map[string][]string). // // NOTE: only exported keys are encoded due to the use of reflection. Unexported // keys are silently discarded. type Encoder struct { // String to use for a single indentation level; default is two spaces. Indent string w *bufio.Writer hasWritten bool // written any output to w yet? } // NewEncoder create a new Encoder. func NewEncoder(w io.Writer) *Encoder { return &Encoder{ w: bufio.NewWriter(w), Indent: " ", } } // Encode writes a TOML representation of the Go value to the Encoder's writer. // // An error is returned if the value given cannot be encoded to a valid TOML // document. func (enc *Encoder) Encode(v interface{}) error { rv := eindirect(reflect.ValueOf(v)) if err := enc.safeEncode(Key([]string{}), rv); err != nil { return err } return enc.w.Flush() } func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { defer func() { if r := recover(); r != nil { if terr, ok := r.(tomlEncodeError); ok { err = terr.error return } panic(r) } }() enc.encode(key, rv) return nil } func (enc *Encoder) encode(key Key, rv reflect.Value) { // Special case: time needs to be in ISO8601 format. // // Special case: if we can marshal the type to text, then we used that. This // prevents the encoder for handling these types as generic structs (or // whatever the underlying type of a TextMarshaler is). switch t := rv.Interface().(type) { case time.Time, encoding.TextMarshaler, Marshaler: enc.writeKeyValue(key, rv, false) return // TODO: #76 would make this superfluous after implemented. case Primitive: enc.encode(key, reflect.ValueOf(t.undecoded)) return } k := rv.Kind() switch k { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: enc.writeKeyValue(key, rv, false) case reflect.Array, reflect.Slice: if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) { enc.eArrayOfTables(key, rv) } else { enc.writeKeyValue(key, rv, false) } case reflect.Interface: if rv.IsNil() { return } enc.encode(key, rv.Elem()) case reflect.Map: if rv.IsNil() { return } enc.eTable(key, rv) case reflect.Ptr: if rv.IsNil() { return } enc.encode(key, rv.Elem()) case reflect.Struct: enc.eTable(key, rv) default: encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k)) } } // eElement encodes any value that can be an array element. func (enc *Encoder) eElement(rv reflect.Value) { switch v := rv.Interface().(type) { case time.Time: // Using TextMarshaler adds extra quotes, which we don't want. format := time.RFC3339Nano switch v.Location() { case internal.LocalDatetime: format = "2006-01-02T15:04:05.999999999" case internal.LocalDate: format = "2006-01-02" case internal.LocalTime: format = "15:04:05.999999999" } switch v.Location() { default: enc.wf(v.Format(format)) case internal.LocalDatetime, internal.LocalDate, internal.LocalTime: enc.wf(v.In(time.UTC).Format(format)) } return case Marshaler: s, err := v.MarshalTOML() if err != nil { encPanic(err) } enc.w.Write(s) return case encoding.TextMarshaler: s, err := v.MarshalText() if err != nil { encPanic(err) } enc.writeQuoted(string(s)) return } switch rv.Kind() { case reflect.String: enc.writeQuoted(rv.String()) case reflect.Bool: enc.wf(strconv.FormatBool(rv.Bool())) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: enc.wf(strconv.FormatInt(rv.Int(), 10)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: enc.wf(strconv.FormatUint(rv.Uint(), 10)) case reflect.Float32: f := rv.Float() if math.IsNaN(f) { enc.wf("nan") } else if math.IsInf(f, 0) { enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)]) } else { enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32))) } case reflect.Float64: f := rv.Float() if math.IsNaN(f) { enc.wf("nan") } else if math.IsInf(f, 0) { enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)]) } else { enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64))) } case reflect.Array, reflect.Slice: enc.eArrayOrSliceElement(rv) case reflect.Struct: enc.eStruct(nil, rv, true) case reflect.Map: enc.eMap(nil, rv, true) case reflect.Interface: enc.eElement(rv.Elem()) default: encPanic(fmt.Errorf("unexpected primitive type: %T", rv.Interface())) } } // By the TOML spec, all floats must have a decimal with at least one number on // either side. func floatAddDecimal(fstr string) string { if !strings.Contains(fstr, ".") { return fstr + ".0" } return fstr } func (enc *Encoder) writeQuoted(s string) { enc.wf("\"%s\"", dblQuotedReplacer.Replace(s)) } func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { length := rv.Len() enc.wf("[") for i := 0; i < length; i++ { elem := rv.Index(i) enc.eElement(elem) if i != length-1 { enc.wf(", ") } } enc.wf("]") } func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { if len(key) == 0 { encPanic(errNoKey) } for i := 0; i < rv.Len(); i++ { trv := rv.Index(i) if isNil(trv) { continue } enc.newline() enc.wf("%s[[%s]]", enc.indentStr(key), key) enc.newline() enc.eMapOrStruct(key, trv, false) } } func (enc *Encoder) eTable(key Key, rv reflect.Value) { if len(key) == 1 { // Output an extra newline between top-level tables. // (The newline isn't written if nothing else has been written though.) enc.newline() } if len(key) > 0 { enc.wf("%s[%s]", enc.indentStr(key), key) enc.newline() } enc.eMapOrStruct(key, rv, false) } func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) { switch rv := eindirect(rv); rv.Kind() { case reflect.Map: enc.eMap(key, rv, inline) case reflect.Struct: enc.eStruct(key, rv, inline) default: // Should never happen? panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) } } func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) { rt := rv.Type() if rt.Key().Kind() != reflect.String { encPanic(errNonString) } // Sort keys so that we have deterministic output. And write keys directly // underneath this key first, before writing sub-structs or sub-maps. var mapKeysDirect, mapKeysSub []string for _, mapKey := range rv.MapKeys() { k := mapKey.String() if typeIsTable(tomlTypeOfGo(rv.MapIndex(mapKey))) { mapKeysSub = append(mapKeysSub, k) } else { mapKeysDirect = append(mapKeysDirect, k) } } var writeMapKeys = func(mapKeys []string, trailC bool) { sort.Strings(mapKeys) for i, mapKey := range mapKeys { val := rv.MapIndex(reflect.ValueOf(mapKey)) if isNil(val) { continue } if inline { enc.writeKeyValue(Key{mapKey}, val, true) if trailC || i != len(mapKeys)-1 { enc.wf(", ") } } else { enc.encode(key.add(mapKey), val) } } } if inline { enc.wf("{") } writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0) writeMapKeys(mapKeysSub, false) if inline { enc.wf("}") } } const is32Bit = (32 << (^uint(0) >> 63)) == 32 func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) { // Write keys for fields directly under this key first, because if we write // a field that creates a new table then all keys under it will be in that // table (not the one we're writing here). // // Fields is a [][]int: for fieldsDirect this always has one entry (the // struct index). For fieldsSub it contains two entries: the parent field // index from tv, and the field indexes for the fields of the sub. var ( rt = rv.Type() fieldsDirect, fieldsSub [][]int addFields func(rt reflect.Type, rv reflect.Value, start []int) ) addFields = func(rt reflect.Type, rv reflect.Value, start []int) { for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) if f.PkgPath != "" && !f.Anonymous { /// Skip unexported fields. continue } opts := getOptions(f.Tag) if opts.skip { continue } frv := rv.Field(i) // Treat anonymous struct fields with tag names as though they are // not anonymous, like encoding/json does. // // Non-struct anonymous fields use the normal encoding logic. if f.Anonymous { t := f.Type switch t.Kind() { case reflect.Struct: if getOptions(f.Tag).name == "" { addFields(t, frv, append(start, f.Index...)) continue } case reflect.Ptr: if t.Elem().Kind() == reflect.Struct && getOptions(f.Tag).name == "" { if !frv.IsNil() { addFields(t.Elem(), frv.Elem(), append(start, f.Index...)) } continue } } } if typeIsTable(tomlTypeOfGo(frv)) { fieldsSub = append(fieldsSub, append(start, f.Index...)) } else { // Copy so it works correct on 32bit archs; not clear why this // is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4 // This also works fine on 64bit, but 32bit archs are somewhat // rare and this is a wee bit faster. if is32Bit { copyStart := make([]int, len(start)) copy(copyStart, start) fieldsDirect = append(fieldsDirect, append(copyStart, f.Index...)) } else { fieldsDirect = append(fieldsDirect, append(start, f.Index...)) } } } } addFields(rt, rv, nil) writeFields := func(fields [][]int) { for _, fieldIndex := range fields { fieldType := rt.FieldByIndex(fieldIndex) fieldVal := rv.FieldByIndex(fieldIndex) if isNil(fieldVal) { /// Don't write anything for nil fields. continue } opts := getOptions(fieldType.Tag) if opts.skip { continue } keyName := fieldType.Name if opts.name != "" { keyName = opts.name } if opts.omitempty && isEmpty(fieldVal) { continue } if opts.omitzero && isZero(fieldVal) { continue } if inline { enc.writeKeyValue(Key{keyName}, fieldVal, true) if fieldIndex[0] != len(fields)-1 { enc.wf(", ") } } else { enc.encode(key.add(keyName), fieldVal) } } } if inline { enc.wf("{") } writeFields(fieldsDirect) writeFields(fieldsSub) if inline { enc.wf("}") } } // tomlTypeOfGo returns the TOML type name of the Go value's type. // // It is used to determine whether the types of array elements are mixed (which // is forbidden). If the Go value is nil, then it is illegal for it to be an // array element, and valueIsNil is returned as true. // // The type may be `nil`, which means no concrete TOML type could be found. func tomlTypeOfGo(rv reflect.Value) tomlType { if isNil(rv) || !rv.IsValid() { return nil } switch rv.Kind() { case reflect.Bool: return tomlBool case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return tomlInteger case reflect.Float32, reflect.Float64: return tomlFloat case reflect.Array, reflect.Slice: if typeEqual(tomlHash, tomlArrayType(rv)) { return tomlArrayHash } return tomlArray case reflect.Ptr, reflect.Interface: return tomlTypeOfGo(rv.Elem()) case reflect.String: return tomlString case reflect.Map: return tomlHash case reflect.Struct: if _, ok := rv.Interface().(time.Time); ok { return tomlDatetime } if isMarshaler(rv) { return tomlString } return tomlHash default: if isMarshaler(rv) { return tomlString } encPanic(errors.New("unsupported type: " + rv.Kind().String())) panic("unreachable") } } func isMarshaler(rv reflect.Value) bool { switch rv.Interface().(type) { case encoding.TextMarshaler: return true case Marshaler: return true } // Someone used a pointer receiver: we can make it work for pointer values. if rv.CanAddr() { if _, ok := rv.Addr().Interface().(encoding.TextMarshaler); ok { return true } if _, ok := rv.Addr().Interface().(Marshaler); ok { return true } } return false } // tomlArrayType returns the element type of a TOML array. The type returned // may be nil if it cannot be determined (e.g., a nil slice or a zero length // slize). This function may also panic if it finds a type that cannot be // expressed in TOML (such as nil elements, heterogeneous arrays or directly // nested arrays of tables). func tomlArrayType(rv reflect.Value) tomlType { if isNil(rv) || !rv.IsValid() || rv.Len() == 0 { return nil } /// Don't allow nil. rvlen := rv.Len() for i := 1; i < rvlen; i++ { if tomlTypeOfGo(rv.Index(i)) == nil { encPanic(errArrayNilElement) } } firstType := tomlTypeOfGo(rv.Index(0)) if firstType == nil { encPanic(errArrayNilElement) } return firstType } type tagOptions struct { skip bool // "-" name string omitempty bool omitzero bool } func getOptions(tag reflect.StructTag) tagOptions { t := tag.Get("toml") if t == "-" { return tagOptions{skip: true} } var opts tagOptions parts := strings.Split(t, ",") opts.name = parts[0] for _, s := range parts[1:] { switch s { case "omitempty": opts.omitempty = true case "omitzero": opts.omitzero = true } } return opts } func isZero(rv reflect.Value) bool { switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return rv.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return rv.Uint() == 0 case reflect.Float32, reflect.Float64: return rv.Float() == 0.0 } return false } func isEmpty(rv reflect.Value) bool { switch rv.Kind() { case reflect.Array, reflect.Slice, reflect.Map, reflect.String: return rv.Len() == 0 case reflect.Bool: return !rv.Bool() } return false } func (enc *Encoder) newline() { if enc.hasWritten { enc.wf("\n") } } // Write a key/value pair: // // key = // // This is also used for "k = v" in inline tables; so something like this will // be written in three calls: // // ┌────────────────────┐ // │ ┌───┐ ┌─────┐│ // v v v v vv // key = {k = v, k2 = v2} // func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) { if len(key) == 0 { encPanic(errNoKey) } enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1)) enc.eElement(val) if !inline { enc.newline() } } func (enc *Encoder) wf(format string, v ...interface{}) { _, err := fmt.Fprintf(enc.w, format, v...) if err != nil { encPanic(err) } enc.hasWritten = true } func (enc *Encoder) indentStr(key Key) string { return strings.Repeat(enc.Indent, len(key)-1) } func encPanic(err error) { panic(tomlEncodeError{err}) } func eindirect(v reflect.Value) reflect.Value { switch v.Kind() { case reflect.Ptr, reflect.Interface: return eindirect(v.Elem()) default: return v } } func isNil(rv reflect.Value) bool { switch rv.Kind() { case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: return rv.IsNil() default: return false } }