Skip to content

Commit 08a5288

Browse files
adityasakyLukas Puehringer
andcommitted
Move cjson code from in-toto-golang
Signed-off-by: Aditya Sirish <aditya@saky.in> Co-authored-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
1 parent 1966c4a commit 08a5288

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed

cjson/canonicaljson.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package cjson
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"reflect"
9+
"regexp"
10+
"sort"
11+
)
12+
13+
/*
14+
encodeCanonicalString is a helper function to canonicalize the passed string
15+
according to the OLPC canonical JSON specification for strings (see
16+
http://wiki.laptop.org/go/Canonical_JSON). String canonicalization consists of
17+
escaping backslashes ("\") and double quotes (") and wrapping the resulting
18+
string in double quotes (").
19+
*/
20+
func encodeCanonicalString(s string) string {
21+
re := regexp.MustCompile(`([\"\\])`)
22+
return fmt.Sprintf("\"%s\"", re.ReplaceAllString(s, "\\$1"))
23+
}
24+
25+
/*
26+
encodeCanonical is a helper function to recursively canonicalize the passed
27+
object according to the OLPC canonical JSON specification (see
28+
http://wiki.laptop.org/go/Canonical_JSON) and write it to the passed
29+
*bytes.Buffer. If canonicalization fails it returns an error.
30+
*/
31+
func encodeCanonical(obj interface{}, result *bytes.Buffer) (err error) {
32+
// Since this function is called recursively, we use panic if an error occurs
33+
// and recover in a deferred function, which is always called before
34+
// returning. There we set the error that is returned eventually.
35+
defer func() {
36+
if r := recover(); r != nil {
37+
err = errors.New(r.(string))
38+
}
39+
}()
40+
41+
switch objAsserted := obj.(type) {
42+
case string:
43+
result.WriteString(encodeCanonicalString(objAsserted))
44+
45+
case bool:
46+
if objAsserted {
47+
result.WriteString("true")
48+
} else {
49+
result.WriteString("false")
50+
}
51+
52+
// The wrapping `EncodeCanonical` function decodes the passed json data with
53+
// `decoder.UseNumber` so that any numeric value is stored as `json.Number`
54+
// (instead of the default `float64`). This allows us to assert that it is a
55+
// non-floating point number, which are the only numbers allowed by the used
56+
// canonicalization specification.
57+
case json.Number:
58+
if _, err := objAsserted.Int64(); err != nil {
59+
panic(fmt.Sprintf("Can't canonicalize floating point number '%s'",
60+
objAsserted))
61+
}
62+
result.WriteString(objAsserted.String())
63+
64+
case nil:
65+
result.WriteString("null")
66+
67+
// Canonicalize slice
68+
case []interface{}:
69+
result.WriteString("[")
70+
for i, val := range objAsserted {
71+
if err := encodeCanonical(val, result); err != nil {
72+
return err
73+
}
74+
if i < (len(objAsserted) - 1) {
75+
result.WriteString(",")
76+
}
77+
}
78+
result.WriteString("]")
79+
80+
case map[string]interface{}:
81+
result.WriteString("{")
82+
83+
// Make a list of keys
84+
var mapKeys []string
85+
for key := range objAsserted {
86+
mapKeys = append(mapKeys, key)
87+
}
88+
// Sort keys
89+
sort.Strings(mapKeys)
90+
91+
// Canonicalize map
92+
for i, key := range mapKeys {
93+
// Note: `key` must be a `string` (see `case map[string]interface{}`) and
94+
// canonicalization of strings cannot err out (see `case string`), thus
95+
// no error handling is needed here.
96+
encodeCanonical(key, result)
97+
98+
result.WriteString(":")
99+
if err := encodeCanonical(objAsserted[key], result); err != nil {
100+
return err
101+
}
102+
if i < (len(mapKeys) - 1) {
103+
result.WriteString(",")
104+
}
105+
i++
106+
}
107+
result.WriteString("}")
108+
109+
default:
110+
// We recover in a deferred function defined above
111+
panic(fmt.Sprintf("Can't canonicalize '%s' of type '%s'",
112+
objAsserted, reflect.TypeOf(objAsserted)))
113+
}
114+
return nil
115+
}
116+
117+
/*
118+
EncodeCanonical JSON canonicalizes the passed object and returns it as a byte
119+
slice. It uses the OLPC canonical JSON specification (see
120+
http://wiki.laptop.org/go/Canonical_JSON). If canonicalization fails the byte
121+
slice is nil and the second return value contains the error.
122+
*/
123+
func EncodeCanonical(obj interface{}) ([]byte, error) {
124+
// FIXME: Terrible hack to turn the passed struct into a map, converting
125+
// the struct's variable names to the json key names defined in the struct
126+
data, err := json.Marshal(obj)
127+
if err != nil {
128+
return nil, err
129+
}
130+
var jsonMap interface{}
131+
132+
dec := json.NewDecoder(bytes.NewReader(data))
133+
dec.UseNumber()
134+
if err := dec.Decode(&jsonMap); err != nil {
135+
return nil, err
136+
}
137+
138+
// Create a buffer and write the canonicalized JSON bytes to it
139+
var result bytes.Buffer
140+
if err := encodeCanonical(jsonMap, &result); err != nil {
141+
return nil, err
142+
}
143+
144+
return result.Bytes(), nil
145+
}

cjson/canonicaljson_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package cjson
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
)
8+
9+
type KeyVal struct {
10+
Private string `json:"private"`
11+
Public string `json:"public"`
12+
Certificate string `json:"certificate,omitempty"`
13+
}
14+
15+
type Key struct {
16+
KeyID string `json:"keyid"`
17+
KeyIDHashAlgorithms []string `json:"keyid_hash_algorithms"`
18+
KeyType string `json:"keytype"`
19+
KeyVal KeyVal `json:"keyval"`
20+
Scheme string `json:"scheme"`
21+
}
22+
23+
func TestEncodeCanonical(t *testing.T) {
24+
objects := []interface{}{
25+
Key{},
26+
Key{
27+
KeyVal: KeyVal{
28+
Private: "priv",
29+
Public: "pub",
30+
},
31+
KeyIDHashAlgorithms: []string{"hash"},
32+
KeyID: "id",
33+
KeyType: "type",
34+
Scheme: "scheme",
35+
},
36+
map[string]interface{}{
37+
"true": true,
38+
"false": false,
39+
"nil": nil,
40+
"int": 3,
41+
"int2": float64(42),
42+
"string": `\"`,
43+
},
44+
Key{
45+
KeyVal: KeyVal{
46+
Certificate: "cert",
47+
Private: "priv",
48+
Public: "pub",
49+
},
50+
KeyIDHashAlgorithms: []string{"hash"},
51+
KeyID: "id",
52+
KeyType: "type",
53+
Scheme: "scheme",
54+
},
55+
}
56+
expectedResult := []string{
57+
`{"keyid":"","keyid_hash_algorithms":null,"keytype":"","keyval":{"private":"","public":""},"scheme":""}`,
58+
`{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"private":"priv","public":"pub"},"scheme":"scheme"}`,
59+
`{"false":false,"int":3,"int2":42,"nil":null,"string":"\\\"","true":true}`,
60+
`{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"certificate":"cert","private":"priv","public":"pub"},"scheme":"scheme"}`,
61+
"",
62+
}
63+
for i := 0; i < len(objects); i++ {
64+
result, err := EncodeCanonical(objects[i])
65+
66+
if string(result) != expectedResult[i] || err != nil {
67+
t.Errorf("EncodeCanonical returned (%s, %s), expected (%s, nil)",
68+
result, err, expectedResult[i])
69+
}
70+
}
71+
}
72+
73+
func TestEncodeCanonicalErr(t *testing.T) {
74+
objects := []interface{}{
75+
map[string]interface{}{"float": 3.14159265359},
76+
TestEncodeCanonical,
77+
}
78+
errPart := []string{
79+
"Can't canonicalize floating point number",
80+
"unsupported type: func(",
81+
}
82+
83+
for i := 0; i < len(objects); i++ {
84+
result, err := EncodeCanonical(objects[i])
85+
if err == nil || !strings.Contains(err.Error(), errPart[i]) {
86+
t.Errorf("EncodeCanonical returned (%s, %s), expected '%s' error",
87+
result, err, errPart[i])
88+
}
89+
}
90+
}
91+
92+
func TestencodeCanonical(t *testing.T) {
93+
expectedError := "Can't canonicalize"
94+
95+
objects := []interface{}{
96+
TestencodeCanonical,
97+
[]interface{}{TestencodeCanonical},
98+
}
99+
100+
for i := 0; i < len(objects); i++ {
101+
var result bytes.Buffer
102+
err := encodeCanonical(objects[i], &result)
103+
if err == nil || !strings.Contains(err.Error(), expectedError) {
104+
t.Errorf("EncodeCanonical returned '%s', expected '%s' error",
105+
err, expectedError)
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)