Skip to content

Commit 4c71fd4

Browse files
authored
Add "newtime" directive to use official messagepack time format (#378)
This adds `msgp:newtime` file directive that will encode all time fields using the -1 extension as defined in the [(revised) messagepack spec](https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type) ReadTime/ReadTimeBytes will now support both types natively, and will accept either as input. Extensions should remain unaffected. Fixes #300
1 parent 62d06cc commit 4c71fd4

File tree

15 files changed

+446
-41
lines changed

15 files changed

+446
-41
lines changed

_generated/newtime.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package _generated
2+
3+
import "time"
4+
5+
//go:generate msgp -v
6+
7+
//msgp:newtime
8+
9+
type NewTime struct {
10+
T time.Time
11+
Array []time.Time
12+
Map map[string]time.Time
13+
}
14+
15+
func (t1 NewTime) Equal(t2 NewTime) bool {
16+
if !t1.T.Equal(t2.T) {
17+
return false
18+
}
19+
if len(t1.Array) != len(t2.Array) {
20+
return false
21+
}
22+
for i := range t1.Array {
23+
if !t1.Array[i].Equal(t2.Array[i]) {
24+
return false
25+
}
26+
}
27+
if len(t1.Map) != len(t2.Map) {
28+
return false
29+
}
30+
for k, v := range t1.Map {
31+
if !t2.Map[k].Equal(v) {
32+
return false
33+
}
34+
}
35+
return true
36+
}

_generated/newtime_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package _generated
2+
3+
import (
4+
"bytes"
5+
"math/rand"
6+
"testing"
7+
"time"
8+
9+
"github.com/tinylib/msgp/msgp"
10+
)
11+
12+
func TestNewTime(t *testing.T) {
13+
value := NewTime{
14+
T: time.Now().UTC(),
15+
Array: []time.Time{time.Now().UTC(), time.Now().UTC()},
16+
Map: map[string]time.Time{
17+
"a": time.Now().UTC(),
18+
},
19+
}
20+
encoded, err := value.MarshalMsg(nil)
21+
if err != nil {
22+
t.Fatal(err)
23+
}
24+
checkExtMinusOne(t, encoded)
25+
var got NewTime
26+
_, err = got.UnmarshalMsg(encoded)
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
if !value.Equal(got) {
31+
t.Errorf("UnmarshalMsg got %v want %v", value, got)
32+
}
33+
34+
var buf bytes.Buffer
35+
w := msgp.NewWriter(&buf)
36+
err = value.EncodeMsg(w)
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
w.Flush()
41+
checkExtMinusOne(t, buf.Bytes())
42+
43+
got = NewTime{}
44+
r := msgp.NewReader(&buf)
45+
err = got.DecodeMsg(r)
46+
if err != nil {
47+
t.Fatal(err)
48+
}
49+
if !value.Equal(got) {
50+
t.Errorf("DecodeMsg got %v want %v", value, got)
51+
}
52+
}
53+
54+
func checkExtMinusOne(t *testing.T, b []byte) {
55+
r := msgp.NewReader(bytes.NewBuffer(b))
56+
_, err := r.ReadMapHeader()
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
key, err := r.ReadMapKey(nil)
61+
if err != nil {
62+
t.Fatal(err)
63+
}
64+
for !bytes.Equal(key, []byte("T")) {
65+
key, err = r.ReadMapKey(nil)
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
}
70+
n, _, err := r.ReadExtensionRaw()
71+
if err != nil {
72+
t.Fatal(err)
73+
}
74+
if n != -1 {
75+
t.Fatalf("got %v want -1", n)
76+
}
77+
t.Log("Was -1 extension")
78+
}
79+
80+
func TestNewTimeRandom(t *testing.T) {
81+
rng := rand.New(rand.NewSource(0))
82+
runs := int(1e6)
83+
if testing.Short() {
84+
runs = 1e4
85+
}
86+
for i := 0; i < runs; i++ {
87+
nanos := rng.Int63n(999999999 + 1)
88+
secs := rng.Uint64()
89+
// Tweak the distribution, so we get more than average number of
90+
// length 4 and 8 timestamps.
91+
if rng.Intn(5) == 0 {
92+
secs %= uint64(time.Now().Unix())
93+
if rng.Intn(2) == 0 {
94+
nanos = 0
95+
}
96+
}
97+
98+
value := NewTime{
99+
T: time.Unix(int64(secs), nanos),
100+
}
101+
encoded, err := value.MarshalMsg(nil)
102+
if err != nil {
103+
t.Fatal(err)
104+
}
105+
var got NewTime
106+
_, err = got.UnmarshalMsg(encoded)
107+
if err != nil {
108+
t.Fatal(err)
109+
}
110+
if !value.Equal(got) {
111+
t.Fatalf("UnmarshalMsg got %v want %v", value, got)
112+
}
113+
var buf bytes.Buffer
114+
w := msgp.NewWriter(&buf)
115+
err = value.EncodeMsg(w)
116+
if err != nil {
117+
t.Fatal(err)
118+
}
119+
w.Flush()
120+
got = NewTime{}
121+
r := msgp.NewReader(&buf)
122+
err = got.DecodeMsg(r)
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
if !value.Equal(got) {
127+
t.Fatalf("DecodeMsg got %v want %v", value, got)
128+
}
129+
}
130+
}

gen/encode.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func (e *encodeGen) writeAndCheck(typ string, argfmt string, arg interface{}) {
3030
if e.ctx.compFloats && typ == "Float64" {
3131
typ = "Float"
3232
}
33+
if e.ctx.newTime && typ == "Time" {
34+
typ = "TimeExt"
35+
}
3336

3437
e.p.printf("\nerr = en.Write%s(%s)", typ, fmt.Sprintf(argfmt, arg))
3538
e.p.wrapErrCheck(e.ctx.ArgsStr())

gen/marshal.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ func (m *marshalGen) rawAppend(typ string, argfmt string, arg interface{}) {
6565
if m.ctx.compFloats && typ == "Float64" {
6666
typ = "Float"
6767
}
68+
if m.ctx.newTime && typ == "Time" {
69+
typ = "TimeExt"
70+
}
71+
6872
m.p.printf("\no = msgp.Append%s(o, %s)", typ, fmt.Sprintf(argfmt, arg))
6973
}
7074

gen/spec.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type Printer struct {
7979
gens []generator
8080
CompactFloats bool
8181
ClearOmitted bool
82+
NewTime bool
8283
}
8384

8485
func NewPrinter(m Method, out io.Writer, tests io.Writer) *Printer {
@@ -148,7 +149,11 @@ func (p *Printer) Print(e Elem) error {
148149
// collisions between idents created during SetVarname and idents created during Print,
149150
// hence the separate prefixes.
150151
resetIdent("zb")
151-
err := g.Execute(e, Context{compFloats: p.CompactFloats, clearOmitted: p.ClearOmitted})
152+
err := g.Execute(e, Context{
153+
compFloats: p.CompactFloats,
154+
clearOmitted: p.ClearOmitted,
155+
newTime: p.NewTime,
156+
})
152157
resetIdent("za")
153158

154159
if err != nil {
@@ -178,6 +183,7 @@ type Context struct {
178183
path []contextItem
179184
compFloats bool
180185
clearOmitted bool
186+
newTime bool
181187
}
182188

183189
func (c *Context) PushString(s string) {

msgp/errors.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,31 @@ func (u UintOverflow) Resumable() bool { return true }
212212

213213
func (u UintOverflow) withContext(ctx string) error { u.ctx = addCtx(u.ctx, ctx); return u }
214214

215+
// InvalidTimestamp is returned when an invalid timestamp is encountered
216+
type InvalidTimestamp struct {
217+
Nanos int64 // value of the nano, if invalid
218+
FieldLength int // Unexpected field length.
219+
ctx string
220+
}
221+
222+
// Error implements the error interface
223+
func (u InvalidTimestamp) Error() (str string) {
224+
if u.Nanos > 0 {
225+
str = "msgp: timestamp nanosecond field value " + strconv.FormatInt(u.Nanos, 10) + " exceeds maximum allows of 999999999"
226+
} else if u.FieldLength >= 0 {
227+
str = "msgp: invalid timestamp field length " + strconv.FormatInt(int64(u.FieldLength), 10) + " - must be 4, 8 or 12"
228+
}
229+
if u.ctx != "" {
230+
str += " at " + u.ctx
231+
}
232+
return str
233+
}
234+
235+
// Resumable is always 'true' for overflows
236+
func (u InvalidTimestamp) Resumable() bool { return true }
237+
238+
func (u InvalidTimestamp) withContext(ctx string) error { u.ctx = addCtx(u.ctx, ctx); return u }
239+
215240
// UintBelowZero is returned when a call
216241
// would cast a signed integer below zero
217242
// to an unsigned integer.

msgp/extension.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ const (
1515

1616
// TimeExtension is the extension number used for time.Time
1717
TimeExtension = 5
18+
19+
// MsgTimeExtension is the extension number for timestamps as defined in
20+
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
21+
MsgTimeExtension = -1
1822
)
1923

24+
// msgTimeExtension is a painful workaround to avoid "constant -1 overflows byte".
25+
var msgTimeExtension = int8(MsgTimeExtension)
26+
2027
// our extensions live here
2128
var extensionReg = make(map[int8]func() Extension)
2229

@@ -477,15 +484,27 @@ func AppendExtension(b []byte, e Extension) ([]byte, error) {
477484
// - InvalidPrefixError
478485
// - An umarshal error returned from e.UnmarshalBinary
479486
func ReadExtensionBytes(b []byte, e Extension) ([]byte, error) {
487+
typ, remain, data, err := readExt(b)
488+
if err != nil {
489+
return b, err
490+
}
491+
if typ != e.ExtensionType() {
492+
return b, errExt(typ, e.ExtensionType())
493+
}
494+
return remain, e.UnmarshalBinary(data)
495+
}
496+
497+
// readExt will read the extension type, and return remaining bytes,
498+
// as well as the data of the extension.
499+
func readExt(b []byte) (typ int8, remain []byte, data []byte, err error) {
480500
l := len(b)
481501
if l < 3 {
482-
return b, ErrShortBytes
502+
return 0, b, nil, ErrShortBytes
483503
}
484504
lead := b[0]
485505
var (
486506
sz int // size of 'data'
487507
off int // offset of 'data'
488-
typ int8
489508
)
490509
switch lead {
491510
case mfixext1:
@@ -513,35 +532,30 @@ func ReadExtensionBytes(b []byte, e Extension) ([]byte, error) {
513532
typ = int8(b[2])
514533
off = 3
515534
if sz == 0 {
516-
return b[3:], e.UnmarshalBinary(b[3:3])
535+
return typ, b[3:], b[3:3], nil
517536
}
518537
case mext16:
519538
if l < 4 {
520-
return b, ErrShortBytes
539+
return 0, b, nil, ErrShortBytes
521540
}
522541
sz = int(big.Uint16(b[1:]))
523542
typ = int8(b[3])
524543
off = 4
525544
case mext32:
526545
if l < 6 {
527-
return b, ErrShortBytes
546+
return 0, b, nil, ErrShortBytes
528547
}
529548
sz = int(big.Uint32(b[1:]))
530549
typ = int8(b[5])
531550
off = 6
532551
default:
533-
return b, badPrefix(ExtensionType, lead)
534-
}
535-
536-
if typ != e.ExtensionType() {
537-
return b, errExt(typ, e.ExtensionType())
552+
return 0, b, nil, badPrefix(ExtensionType, lead)
538553
}
539-
540554
// the data of the extension starts
541555
// at 'off' and is 'sz' bytes long
556+
tot := off + sz
542557
if len(b[off:]) < sz {
543-
return b, ErrShortBytes
558+
return 0, b, nil, ErrShortBytes
544559
}
545-
tot := off + sz
546-
return b[tot:], e.UnmarshalBinary(b[off:tot])
560+
return typ, b[tot:], b[off:tot:tot], nil
547561
}

msgp/json_bytes.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func writeNext(w jsWriter, msg []byte, scratch []byte, depth int) ([]byte, []byt
7272
if err != nil {
7373
return nil, scratch, err
7474
}
75-
if et == TimeExtension {
75+
if et == TimeExtension || et == MsgTimeExtension {
7676
t = TimeType
7777
}
7878
}
@@ -276,7 +276,7 @@ func rwExtensionBytes(w jsWriter, msg []byte, scratch []byte, depth int) ([]byte
276276
}
277277

278278
// if it's time.Time
279-
if et == TimeExtension {
279+
if et == TimeExtension || et == MsgTimeExtension {
280280
var tm time.Time
281281
tm, msg, err = ReadTimeBytes(msg)
282282
if err != nil {

0 commit comments

Comments
 (0)