Skip to content

Commit 34fa67d

Browse files
committed
Fix edge cases around special floats in JSONified keys
1 parent 7d07508 commit 34fa67d

12 files changed

+228
-103
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@ Preserves map order.
4343
-h Show this help message
4444
-v Show version
4545
```
46+
47+
Packages contained in this repo may also be used to convert all supported data formats to a normalized tree of ordered Go objects.
48+
See [godoc](https://godoc.org/github.com/sclevine/yj) for details.

convert/encoding.go

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,56 @@
11
package convert
22

3-
import "io"
3+
import (
4+
"io"
5+
"math"
6+
)
47

58
type Encoding interface {
69
String() string
710
Encode(w io.Writer, in interface{}) error
811
Decode(r io.Reader) (interface{}, error)
912
}
13+
14+
type SpecialFloats int
15+
16+
const (
17+
FloatsReal SpecialFloats = iota
18+
FloatsString
19+
FloatsNumber
20+
)
21+
22+
func (s SpecialFloats) NaN() interface{} {
23+
switch s {
24+
case FloatsReal:
25+
return math.NaN()
26+
case FloatsString:
27+
return "NaN"
28+
case FloatsNumber:
29+
return (*float64)(nil)
30+
}
31+
panic("NaN: invalid special float type")
32+
}
33+
34+
func (s SpecialFloats) PosInf() interface{} {
35+
switch s {
36+
case FloatsReal:
37+
return math.Inf(1)
38+
case FloatsString:
39+
return "Infinity"
40+
case FloatsNumber:
41+
return math.MaxFloat64
42+
}
43+
panic("PosInf: invalid special float type")
44+
}
45+
46+
func (s SpecialFloats) NegInf() interface{} {
47+
switch s {
48+
case FloatsReal:
49+
return math.Inf(-1)
50+
case FloatsString:
51+
return "-Infinity"
52+
case FloatsNumber:
53+
return -math.MaxFloat64
54+
}
55+
panic("NegInf: invalid special float type")
56+
}

convert/toml.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
)
1010

1111
type TOML struct {
12-
FloatStrings bool
13-
Indent bool
12+
SpecialFloats
13+
Indent bool
1414
}
1515

1616
func (TOML) String() string {
@@ -23,7 +23,11 @@ func (t TOML) Encode(w io.Writer, in interface{}) error {
2323
if !t.Indent {
2424
tomlEnc.Indentation("")
2525
}
26-
enc := toml.Encoder{FloatStrings: t.FloatStrings}
26+
enc := toml.Encoder{
27+
NaN: t.NaN(),
28+
PosInf: t.PosInf(),
29+
NegInf: t.NegInf(),
30+
}
2731
out, err := enc.Encode(in)
2832
if err != nil {
2933
return err
@@ -57,8 +61,10 @@ func (t TOML) Decode(r io.Reader) (interface{}, error) {
5761
if err != nil {
5862
return nil, err
5963
}
60-
dec := toml.Decoder{FloatStrings: t.FloatStrings}
64+
dec := toml.Decoder{
65+
NaN: t.NaN(),
66+
PosInf: t.PosInf(),
67+
NegInf: t.NegInf(),
68+
}
6169
return dec.Decode(tree)
6270
}
63-
64-

convert/yaml.go

+17-17
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,31 @@ package convert
22

33
import (
44
"io"
5-
"math"
65

76
goyaml "gopkg.in/yaml.v3"
87

98
"github.com/sclevine/yj/yaml"
109
)
1110

1211
type YAML struct {
13-
FloatStrings bool
14-
JSONKeys bool
15-
EscapeHTML bool
12+
SpecialFloats
13+
KeySpecialFloats SpecialFloats
14+
JSONKeys bool
15+
EscapeHTML bool
1616
}
1717

1818
func (YAML) String() string {
1919
return "YAML"
2020
}
2121

2222
func (y YAML) Encode(w io.Writer, in interface{}) error {
23-
enc := &yaml.Encoder{}
24-
if y.FloatStrings {
25-
enc.NaN = "NaN"
26-
enc.PosInf = "Infinity"
27-
enc.NegInf = "-Infinity"
23+
enc := &yaml.Encoder{
24+
NaN: y.NaN(),
25+
PosInf: y.PosInf(),
26+
NegInf: y.NegInf(),
27+
KeyNaN: y.KeySpecialFloats.NaN(),
28+
KeyPosInf: y.KeySpecialFloats.PosInf(),
29+
KeyNegInf: y.KeySpecialFloats.NegInf(),
2830
}
2931
if y.JSONKeys {
3032
enc.KeyUnmarshal = (&yaml.KeyJSON{}).Unmarshal
@@ -45,14 +47,12 @@ func (y YAML) Decode(r io.Reader) (interface{}, error) {
4547
}
4648
dec := &yaml.Decoder{
4749
KeyMarshal: (&yaml.KeyJSON{EscapeHTML: y.EscapeHTML}).Marshal, // FIXME: double-check map-keys
48-
NaN: (*float64)(nil),
49-
PosInf: math.MaxFloat64,
50-
NegInf: -math.MaxFloat64,
51-
}
52-
if y.FloatStrings {
53-
dec.NaN = "NaN"
54-
dec.PosInf = "Infinity"
55-
dec.NegInf = "-Infinity"
50+
NaN: y.NaN(),
51+
PosInf: y.PosInf(),
52+
NegInf: y.NegInf(),
53+
KeyNaN: y.KeySpecialFloats.NaN(),
54+
KeyPosInf: y.KeySpecialFloats.PosInf(),
55+
KeyNegInf: y.KeySpecialFloats.NegInf(),
5656
}
5757
return dec.Decode(&node)
5858
}

flags.go

+53-17
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,15 @@ func flagFilter(r rune) rune {
6161
func transform(s string) (from, to convert.Encoding, err error) {
6262
escapeHTML := strings.ContainsRune(s, FlagEscapeHTML)
6363
indent := strings.ContainsRune(s, FlagIndent)
64-
floatStrings := !strings.ContainsRune(s, FlagNoFloatStrings)
6564
jsonKeys := strings.ContainsRune(s, FlagJSONKeys)
65+
floatStrings := !strings.ContainsRune(s, FlagNoFloatStrings)
6666

6767
yaml := &convert.YAML{
68-
FloatStrings: floatStrings,
69-
JSONKeys: jsonKeys,
70-
EscapeHTML: escapeHTML,
68+
JSONKeys: jsonKeys,
69+
EscapeHTML: escapeHTML,
7170
}
7271
toml := &convert.TOML{
73-
FloatStrings: floatStrings,
74-
Indent: indent,
72+
Indent: indent,
7573
}
7674
json := &convert.JSON{
7775
EscapeHTML: escapeHTML,
@@ -100,6 +98,44 @@ func transform(s string) (from, to convert.Encoding, err error) {
10098
from, to = to, json
10199
}
102100

101+
setKeyFloats(to, func(setTo func(v convert.SpecialFloats)) {
102+
if floatStrings {
103+
setTo(convert.FloatsString)
104+
} else {
105+
// never convert number to nan, inf, -inf
106+
setTo(convert.FloatsReal)
107+
}
108+
})
109+
110+
setKeyFloats(from, func(setFrom func(v convert.SpecialFloats)) {
111+
if floatStrings {
112+
setFrom(convert.FloatsString)
113+
} else {
114+
setFrom(convert.FloatsNumber)
115+
}
116+
})
117+
118+
setFloats(to, func(setTo func(v convert.SpecialFloats)) {
119+
if floatStrings {
120+
setTo(convert.FloatsString)
121+
} else {
122+
// never convert number to nan, inf, -inf
123+
setTo(convert.FloatsReal)
124+
}
125+
})
126+
127+
setFloats(from, func(setFrom func(v convert.SpecialFloats)) {
128+
if floatStrings {
129+
setFrom(convert.FloatsString)
130+
} else {
131+
setFrom(convert.FloatsNumber)
132+
}
133+
setFloats(to, func(setTo func(v convert.SpecialFloats)) {
134+
setFrom(convert.FloatsReal)
135+
setTo(convert.FloatsReal)
136+
})
137+
})
138+
103139
if _, toYAML := to.(*convert.YAML); jsonKeys && !toYAML {
104140
err = fmt.Errorf("flag -%c only valid for YAML output", FlagJSONKeys)
105141
return
@@ -118,23 +154,23 @@ func transform(s string) (from, to convert.Encoding, err error) {
118154
}
119155
}
120156

121-
floatOff(to, func(toOff func()) {
122-
floatOff(from, func(fromOff func()) {
123-
toOff()
124-
fromOff()
125-
})
126-
})
127-
128-
// FIXME: validate -n isn't used between inapplicable types
157+
// TODO: validate -n isn't used between inapplicable types
129158

130159
return
131160
}
132161

133-
func floatOff(e convert.Encoding, f func(off func())) {
162+
func setFloats(e convert.Encoding, f func(set func(v convert.SpecialFloats))) {
134163
switch e := e.(type) {
135164
case *convert.YAML:
136-
f(func() { e.FloatStrings = false })
165+
f(func(v convert.SpecialFloats) { e.SpecialFloats = v })
137166
case *convert.TOML:
138-
f(func() { e.FloatStrings = false })
167+
f(func(v convert.SpecialFloats) { e.SpecialFloats = v })
168+
}
169+
}
170+
171+
func setKeyFloats(e convert.Encoding, f func(set func(v convert.SpecialFloats))) {
172+
switch e := e.(type) {
173+
case *convert.YAML:
174+
f(func(v convert.SpecialFloats) { e.KeySpecialFloats = v })
139175
}
140176
}

flags_test.go

+16-15
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,33 @@ import (
1010
func TestParse(t *testing.T) {
1111
config, err := main.Parse("-t", "y\tk-", "kn-k ", "h h", "")
1212
assertEq(t, err, nil)
13-
toml, ok := config.From.(convert.TOML)
13+
toml, ok := config.From.(*convert.TOML)
1414
assertEq(t, ok, true)
15-
assertEq(t, toml, convert.TOML{
16-
FloatStrings: false,
15+
assertEq(t, toml, &convert.TOML{
16+
SpecialFloats: convert.FloatsReal,
1717
})
18-
yaml, ok := config.To.(convert.YAML)
18+
yaml, ok := config.To.(*convert.YAML)
1919
assertEq(t, ok, true)
20-
assertEq(t, yaml, convert.YAML{
21-
EscapeHTML: false,
22-
FloatStrings: false,
23-
JSONKeys: true,
20+
assertEq(t, yaml, &convert.YAML{
21+
SpecialFloats: convert.FloatsReal,
22+
EscapeHTML: false,
23+
JSONKeys: true,
2424
})
2525
assertEq(t, config.Help, true)
2626

2727
config, err = main.Parse("--\t\te ", "")
2828
assertEq(t, err, nil)
29-
yaml, ok = config.From.(convert.YAML)
29+
yaml, ok = config.From.(*convert.YAML)
3030
assertEq(t, ok, true)
31-
assertEq(t, yaml, convert.YAML{
32-
EscapeHTML: true,
33-
FloatStrings: true,
34-
JSONKeys: false,
31+
assertEq(t, yaml, &convert.YAML{
32+
SpecialFloats: convert.FloatsString,
33+
KeySpecialFloats: convert.FloatsString,
34+
EscapeHTML: true,
35+
JSONKeys: false,
3536
})
36-
json, ok := config.To.(convert.JSON)
37+
json, ok := config.To.(*convert.JSON)
3738
assertEq(t, ok, true)
38-
assertEq(t, json, convert.JSON{
39+
assertEq(t, json, &convert.JSON{
3940
EscapeHTML: true,
4041
})
4142
assertEq(t, config.Help, false)

testdata/case3_out_tyk.yml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
0: 0
2+
1: 1
3+
1.1: 1.1
4+
key: '"value"'
5+
<: '>'
6+
? <: '>'
7+
: '>'
8+
? key: value
9+
: value
10+
.inf: .inf
11+
-.inf: -.inf
12+
+Infinity: .inf
13+
.nan: .nan
14+
-NaN: .nan
15+
+NaN: .nan
16+
? .inf: .inf
17+
: .inf
18+
? -.inf: -.inf
19+
: -.inf
20+
? .nan: .nan
21+
: .nan
22+
key-number-list:
23+
- 0
24+
- 1
25+
- 1.1
26+
key-string-list:
27+
- a
28+
- b
29+
date: 2006-01-02T15:04:05-07:00
30+
key-obj-list:
31+
- key-string: value-string
32+
key-number: 2

toml/decoder.go

+10-19
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import (
1010
)
1111

1212
type Decoder struct {
13-
FloatStrings bool
13+
// If set, NaN, Inf, etc. are replaced by the set values
14+
NaN, PosInf, NegInf interface{}
1415
}
1516

1617
func (d *Decoder) Decode(toml interface{}) (normal interface{}, err error) {
@@ -75,25 +76,15 @@ func (d Decoder) tomlToSimple(v interface{}) (out interface{}) {
7576
func (d Decoder) postprocess(in interface{}) interface{} {
7677
switch in := in.(type) {
7778
case float64:
78-
if d.FloatStrings {
79-
switch {
80-
case math.IsNaN(in):
81-
return "NaN"
82-
case math.IsInf(in, 1):
83-
return "Infinity"
84-
case math.IsInf(in, -1):
85-
return "-Infinity"
86-
}
87-
} else {
88-
switch {
89-
case math.IsNaN(in):
90-
return (*float64)(nil)
91-
case math.IsInf(in, 1):
92-
return math.MaxFloat64
93-
case math.IsInf(in, -1):
94-
return -math.MaxFloat64
95-
}
79+
switch {
80+
case d.NaN != nil && math.IsNaN(in):
81+
return d.NaN
82+
case d.PosInf != nil && math.IsInf(in, 1):
83+
return d.PosInf
84+
case d.NegInf != nil && math.IsInf(in, -1):
85+
return d.NegInf
9686
}
87+
return in
9788
}
9889
return in
9990
}

0 commit comments

Comments
 (0)