Skip to content

Commit 994000d

Browse files
authored
Merge pull request #10 from adityasaky/add-cjson
Move cjson code from in-toto-golang
2 parents 1966c4a + 68a4286 commit 994000d

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed

cjson/canonicaljson.go

+145
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

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

0 commit comments

Comments
 (0)