Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to use new toml-test, fix a few bugs #293

Merged
merged 1 commit into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 15 additions & 37 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,41 +1,19 @@
TOML_TESTDIR?=tests
# toml: TOML array element cannot contain a table
# Dotted keys are not supported yet.
SKIP_ENCODE?=valid/inline-table-nest,valid/key-dotted

# TODO: these should be fixed
SKIP_DECODE?=valid/array-table-array-string-backslash,\
valid/inline-table-array,\
valid/inline-table,\
valid/nested-inline-table-array,\
invalid/bad-utf8,\
invalid/key-multiline,\
invalid/inline-table-empty,\
invalid/inline-table-nest,\
valid/inline-table-empty,\
valid/inline-table-nest
# Dotted keys are not supported yet.
SKIP_DECODE=valid/key-dotted

SKIP_ENCODE?=valid/inline-table-array,\
valid/inline-table,\
valid/nested-inline-table-array,\
valid/array-table-array-string-backslash,\
valid/inline-table-empty,\
valid/inline-table-nest,\
valid/key-escapes
# No easy way to see if this was a datetime or local datetime; we should extend
# meta with new types for this, which seems like a good idea in any case.
SKIP_DECODE+=,valid/datetime-local-date,valid/datetime-local-time,valid/datetime-local
SKIP_ENCODE+=,valid/datetime-local-date,valid/datetime-local-time,valid/datetime-local

install:
all:
@e=0 # So it won't stop on the first command that fails.
@go install ./...

test: install
@go test ./...
@toml-test -testdir="${TOML_TESTDIR}" -skip="${SKIP_DECODE}" toml-test-decoder
@toml-test -testdir="${TOML_TESTDIR}" -skip="${SKIP_ENCODE}" -encoder toml-test-encoder

fmt:
gofmt -w *.go */*.go
colcheck *.go */*.go

tags:
find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS

push:
git push origin master
git push github master

@go test ./... || e=1
@toml-test -skip="${SKIP_DECODE}" toml-test-decoder || e=1
@toml-test -encoder -skip="${SKIP_ENCODE}" toml-test-encoder || e=1
@exit $e
29 changes: 15 additions & 14 deletions cmd/toml-test-decoder/main.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Command toml-test-decoder satisfies the toml-test interface for testing
// TOML decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
// Command toml-test-decoder satisfies the toml-test interface for testing TOML
// decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
package main

import (
"encoding/json"
"flag"
"fmt"
"log"
"math"
"os"
"path"
"time"
Expand All @@ -16,15 +17,13 @@ import (

func init() {
log.SetFlags(0)

flag.Usage = usage
flag.Parse()
}

func usage() {
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()

os.Exit(1)
}

Expand All @@ -33,19 +32,23 @@ func main() {
flag.Usage()
}

var tmp interface{}
if _, err := toml.DecodeReader(os.Stdin, &tmp); err != nil {
var decoded interface{}
if _, err := toml.DecodeReader(os.Stdin, &decoded); err != nil {
log.Fatalf("Error decoding TOML: %s", err)
}

typedTmp := translate(tmp)
if err := json.NewEncoder(os.Stdout).Encode(typedTmp); err != nil {
j := json.NewEncoder(os.Stdout)
j.SetIndent("", " ")
if err := j.Encode(translate(decoded)); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
}
}

func translate(tomlData interface{}) interface{} {
switch orig := tomlData.(type) {
default:
panic(fmt.Sprintf("Unknown type: %T", tomlData))

case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
Expand All @@ -63,23 +66,21 @@ func translate(tomlData interface{}) interface{} {
for i, v := range orig {
typed[i] = translate(v)
}

// We don't really need to tag arrays, but let's be future proof.
// (If TOML ever supports tuples, we'll need this.)
return tag("array", typed)
return typed
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
if math.IsNaN(orig) {
return tag("float", "nan")
}
return tag("float", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
}

panic(fmt.Sprintf("Unknown type: %T", tomlData))
}

func tag(typeName string, data interface{}) map[string]interface{} {
Expand Down
33 changes: 6 additions & 27 deletions cmd/toml-test-encoder/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Command toml-test-encoder satisfies the toml-test interface for testing
// TOML encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
// Command toml-test-encoder satisfies the toml-test interface for testing TOML
// encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
package main

import (
Expand All @@ -16,15 +16,13 @@ import (

func init() {
log.SetFlags(0)

flag.Usage = usage
flag.Parse()
}

func usage() {
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()

os.Exit(1)
}

Expand All @@ -38,8 +36,7 @@ func main() {
log.Fatalf("Error decoding JSON: %s", err)
}

tomlData := translate(tmp)
if err := toml.NewEncoder(os.Stdout).Encode(tomlData); err != nil {
if err := toml.NewEncoder(os.Stdout).Encode(translate(tmp)); err != nil {
log.Fatalf("Error encoding TOML: %s", err)
}
}
Expand All @@ -56,17 +53,11 @@ func translate(typedJson interface{}) interface{} {
}
return m
case []interface{}:
tabArray := make([]map[string]interface{}, len(v))
a := make([]interface{}, len(v))
for i := range v {
if m, ok := translate(v[i]).(map[string]interface{}); ok {
tabArray[i] = m
} else {
log.Fatalf("JSON arrays may only contain objects. This " +
"corresponds to only tables being allowed in " +
"TOML table arrays.")
}
a[i] = translate(v[i])
}
return tabArray
return a
}
log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
panic("unreachable")
Expand Down Expand Up @@ -108,18 +99,6 @@ func untag(typed map[string]interface{}) interface{} {
return false
}
log.Fatalf("Could not parse '%s' as a boolean.", v)
case "array":
v := v.([]interface{})
array := make([]interface{}, len(v))
for i := range v {
if m, ok := v[i].(map[string]interface{}); ok {
array[i] = untag(m)
} else {
log.Fatalf("Arrays may only contain other arrays or "+
"primitive values, but found a '%T'.", m)
}
}
return array
}
log.Fatalf("Unrecognized tag type '%s'.", t)
panic("unreachable")
Expand Down
9 changes: 6 additions & 3 deletions decode_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ func (md *MetaData) IsDefined(key ...string) bool {

// Type returns a string representation of the type of the key specified.
//
// Type will return the empty string if given an empty key or a key that
// does not exist. Keys are case sensitive.
// Type will return the empty string if given an empty key or a key that does
// not exist. Keys are case sensitive.
func (md *MetaData) Type(key ...string) string {
fullkey := strings.Join(key, ".")
if typ, ok := md.types[fullkey]; ok {
Expand All @@ -68,6 +68,9 @@ func (k Key) maybeQuotedAll() string {
}

func (k Key) maybeQuoted(i int) string {
if k[i] == "" {
return `""`
}
quote := false
for _, c := range k[i] {
if !isBareKeyChar(c) {
Expand All @@ -76,7 +79,7 @@ func (k Key) maybeQuoted(i int) string {
}
}
if quote {
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
return `"` + quotedReplacer.Replace(k[i]) + `"`
}
return k[i]
}
Expand Down
7 changes: 4 additions & 3 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1031,9 +1031,10 @@ func TestDecodeErrors(t *testing.T) {
{"x = \n", "expected value but found '\\n' instead", true},

// Cases found by fuzzing in #155 and #239
{`""` + "\ufffd", "expected key separator", false}, // used to panic with index out of range
{`x="""`, "unexpected EOF", true}, // used to hang
{`x = [{ key = 42 #`, "expected a comma or an inline table terminator", true}, // panic
{`""` + "\ufffd", "invalid UTF-8", false},
{`""=` + "\ufffd", "invalid UTF-8", false},
{`x="""`, "unexpected EOF", true},
{`x = [{ key = 42 #`, "expected a comma or an inline table terminator", true},
{`x = {a = 42 #`, "expected a comma or an inline table terminator '}', but got end of file instead", true},
{`x = [42 #`, "expected a comma or array terminator ']', but got end of file instead", false},

Expand Down
77 changes: 54 additions & 23 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"math"
"reflect"
"sort"
"strconv"
Expand All @@ -32,11 +33,41 @@ var (
)

var quotedReplacer = strings.NewReplacer(
"\t", "\\t",
"\n", "\\n",
"\r", "\\r",
"\"", "\\\"",
"\\", "\\\\",
"\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`,
)

// Encoder controls the encoding of Go values to a TOML document to some
Expand Down Expand Up @@ -187,9 +218,23 @@ func (enc *Encoder) eElement(rv reflect.Value) {
reflect.Uint32, reflect.Uint64:
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
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:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
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.Interface:
Expand Down Expand Up @@ -236,7 +281,6 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
if isNil(trv) {
continue
}
panicIfInvalidKey(key)
enc.newline()
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
Expand All @@ -245,7 +289,6 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
}

func (enc *Encoder) eTable(key Key, rv reflect.Value) {
panicIfInvalidKey(key)
if len(key) == 1 {
// Output an extra newline between top-level tables.
// (The newline isn't written if nothing else has been written though.)
Expand Down Expand Up @@ -465,8 +508,10 @@ func tomlArrayType(rv reflect.Value) tomlType {
encPanic(errArrayMixedElementTypes)
}
}
// If we have a nested array, then we must make sure that the nested
// array contains ONLY primitives.

// If we have a nested array, then we must make sure that the nested array
// contains ONLY primitives.
//
// This checks arbitrarily nested arrays.
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
nest := tomlArrayType(eindirect(rv.Index(0)))
Expand Down Expand Up @@ -535,7 +580,6 @@ func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
panicIfInvalidKey(key)
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val)
enc.newline()
Expand Down Expand Up @@ -573,16 +617,3 @@ func isNil(rv reflect.Value) bool {
return false
}
}

func panicIfInvalidKey(key Key) {
for _, k := range key {
if len(k) == 0 {
encPanic(e("Key '%s' is not a valid table name. Key names "+
"cannot be empty.", key.maybeQuotedAll()))
}
}
}

func isValidKeyName(s string) bool {
return len(s) != 0
}
Loading