Index: service/rawdatastore/serialize.go |
diff --git a/service/rawdatastore/serialize.go b/service/rawdatastore/serialize.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..f61054ccd670956b81d06750c59309949e968fbf |
--- /dev/null |
+++ b/service/rawdatastore/serialize.go |
@@ -0,0 +1,377 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package rawdatastore |
+ |
+import ( |
+ "bytes" |
+ "errors" |
+ "fmt" |
+ "sort" |
+ "time" |
+ |
+ "github.com/luci/gae/service/blobstore" |
+ "github.com/luci/luci-go/common/cmpbin" |
+) |
+ |
+// WritePropertyMapDeterministic allows tests to make WritePropertyMap |
+// deterministic. |
+var WritePropertyMapDeterministic = false |
+ |
+// ReadPropertyMapReasonableLimit sets a limit on the number of rows and |
+// number of properties per row which can be read by ReadPropertyMap. The |
+// total number of Property objects readable by this method is this number |
+// squared (e.g. Limit rows * Limit properties) |
+const ReadPropertyMapReasonableLimit uint64 = 30000 |
+ |
+// ReadKeyNumToksReasonableLimit is the maximum number of Key tokens that |
+// ReadKey is willing to read for a single key. |
+const ReadKeyNumToksReasonableLimit uint64 = 50 |
+ |
+// KeyContext controls whether the various Write and Read serializtion |
+// routines should encode the context of Keys (read: the appid and namespace). |
+// Frequently the appid and namespace of keys are known in advance and so there's |
+// no reason to redundantly encode them. |
+type KeyContext bool |
+ |
+// With- and WithoutContext indicate if the serialization method should include |
+// context for Keys. See KeyContext for more information. |
+const ( |
+ WithContext KeyContext = true |
+ WithoutContext = false |
+) |
+ |
+// WriteKey encodes a key to the buffer. If context is WithContext, then this |
+// encoded value will include the appid and namespace of the key. |
+func WriteKey(buf Buffer, context KeyContext, k Key) (err error) { |
+ // [appid ++ namespace]? ++ #tokens ++ tokens* |
+ defer recoverTo(&err) |
+ appid, namespace, toks := KeySplit(k) |
+ if context == WithContext { |
+ panicIf(buf.WriteByte(1)) |
+ _, e := cmpbin.WriteString(buf, appid) |
+ panicIf(e) |
+ _, e = cmpbin.WriteString(buf, namespace) |
+ panicIf(e) |
+ } else { |
+ panicIf(buf.WriteByte(0)) |
+ } |
+ _, e := cmpbin.WriteUint(buf, uint64(len(toks))) |
+ panicIf(e) |
+ for _, tok := range toks { |
+ panicIf(WriteKeyTok(buf, tok)) |
+ } |
+ return nil |
+} |
+ |
+// ReadKey deserializes a key from the buffer. The value of context must match |
+// the value of context that was passed to WriteKey when the key was encoded. |
+// If context == WithoutContext, then the appid and namespace parameters are |
+// used in the decoded Key. Otherwise they're ignored. |
+func ReadKey(buf Buffer, context KeyContext, appid, namespace string) (ret Key, err error) { |
+ defer recoverTo(&err) |
+ actualCtx, e := buf.ReadByte() |
+ panicIf(e) |
+ |
+ actualAid, actualNS := "", "" |
+ if actualCtx == 1 { |
+ actualAid, _, e = cmpbin.ReadString(buf) |
+ panicIf(e) |
+ actualNS, _, e = cmpbin.ReadString(buf) |
+ panicIf(e) |
+ } else if actualCtx != 0 { |
+ err = fmt.Errorf("helper: expected actualCtx to be 0 or 1, got %d", actualCtx) |
+ return |
+ } |
+ |
+ if context == WithoutContext { |
+ // overrwrite with the supplied ones |
+ actualAid = appid |
+ actualNS = namespace |
+ } |
+ |
+ numToks, _, e := cmpbin.ReadUint(buf) |
+ panicIf(e) |
+ if numToks > ReadKeyNumToksReasonableLimit { |
+ err = fmt.Errorf("helper: tried to decode huge key of length %d", numToks) |
+ return |
+ } |
+ |
+ toks := make([]KeyTok, numToks) |
+ for i := uint64(0); i < numToks; i++ { |
+ toks[i], e = ReadKeyTok(buf) |
+ panicIf(e) |
+ } |
+ |
+ return NewKeyToks(actualAid, actualNS, toks), nil |
+} |
+ |
+// WriteKeyTok writes a KeyTok to the buffer. You usually want WriteKey |
+// instead of this. |
+func WriteKeyTok(buf Buffer, tok KeyTok) (err error) { |
+ // tok.kind ++ typ ++ [tok.stringID || tok.intID] |
+ defer recoverTo(&err) |
+ _, e := cmpbin.WriteString(buf, tok.Kind) |
+ panicIf(e) |
+ if tok.StringID != "" { |
+ panicIf(buf.WriteByte(byte(PTString))) |
+ _, e := cmpbin.WriteString(buf, tok.StringID) |
+ panicIf(e) |
+ } else { |
+ panicIf(buf.WriteByte(byte(PTInt))) |
+ _, e := cmpbin.WriteInt(buf, tok.IntID) |
+ panicIf(e) |
+ } |
+ return nil |
+} |
+ |
+// ReadKeyTok reads a KeyTok from the buffer. You usually want ReadKey |
+// instead of this. |
+func ReadKeyTok(buf Buffer) (ret KeyTok, err error) { |
+ defer recoverTo(&err) |
+ e := error(nil) |
+ ret.Kind, _, e = cmpbin.ReadString(buf) |
+ panicIf(e) |
+ |
+ typ, e := buf.ReadByte() |
+ panicIf(e) |
+ |
+ switch PropertyType(typ) { |
+ case PTString: |
+ ret.StringID, _, err = cmpbin.ReadString(buf) |
+ case PTInt: |
+ ret.IntID, _, err = cmpbin.ReadInt(buf) |
+ if err == nil && ret.IntID <= 0 { |
+ err = errors.New("helper: decoded key with empty stringID and zero/negative intID") |
+ } |
+ default: |
+ err = fmt.Errorf("helper: invalid type %s", PropertyType(typ)) |
+ } |
+ return |
+} |
+ |
+// WriteGeoPoint writes a GeoPoint to the buffer. |
+func WriteGeoPoint(buf Buffer, gp GeoPoint) (err error) { |
+ defer recoverTo(&err) |
+ _, e := cmpbin.WriteFloat64(buf, gp.Lat) |
+ panicIf(e) |
+ _, e = cmpbin.WriteFloat64(buf, gp.Lng) |
+ return e |
+} |
+ |
+// ReadGeoPoint reads a GeoPoint from the buffer. |
+func ReadGeoPoint(buf Buffer) (gp GeoPoint, err error) { |
+ defer recoverTo(&err) |
+ e := error(nil) |
+ gp.Lat, _, e = cmpbin.ReadFloat64(buf) |
+ panicIf(e) |
+ |
+ gp.Lng, _, e = cmpbin.ReadFloat64(buf) |
+ panicIf(e) |
+ |
+ if !gp.Valid() { |
+ err = fmt.Errorf("helper: decoded invalid GeoPoint: %v", gp) |
+ } |
+ return |
+} |
+ |
+// WriteTime writes a time.Time in a byte-sortable way. |
+// |
+// This method truncates the time to microseconds and drops the timezone, |
+// because that's the (undocumented) way that the appengine SDK does it. |
+func WriteTime(buf Buffer, t time.Time) error { |
+ name, off := t.Zone() |
+ if name != "UTC" || off != 0 { |
+ panic(fmt.Errorf("helper: UTC OR DEATH: %s", t)) |
+ } |
+ _, err := cmpbin.WriteUint(buf, uint64(t.Unix())*1e6+uint64(t.Nanosecond()/1e3)) |
+ return err |
+} |
+ |
+// ReadTime reads a time.Time from the buffer. |
+func ReadTime(buf Buffer) (time.Time, error) { |
+ v, _, err := cmpbin.ReadUint(buf) |
+ if err != nil { |
+ return time.Time{}, err |
+ } |
+ return time.Unix(int64(v/1e6), int64((v%1e6)*1e3)).UTC(), nil |
+} |
+ |
+// WriteProperty writes a Property to the buffer. `context` behaves the same |
+// way that it does for WriteKey, but only has an effect if `p` contains a |
+// Key as its Value. |
+func WriteProperty(buf Buffer, p Property, context KeyContext) (err error) { |
+ defer recoverTo(&err) |
+ typb := byte(p.Type()) |
+ if p.IndexSetting() == NoIndex { |
+ typb |= 0x80 |
+ } |
+ panicIf(buf.WriteByte(typb)) |
+ switch p.Type() { |
+ case PTNull, PTBoolTrue, PTBoolFalse: |
+ case PTInt: |
+ _, err = cmpbin.WriteInt(buf, p.Value().(int64)) |
+ case PTFloat: |
+ _, err = cmpbin.WriteFloat64(buf, p.Value().(float64)) |
+ case PTString: |
+ _, err = cmpbin.WriteString(buf, p.Value().(string)) |
+ case PTBytes: |
+ if p.IndexSetting() == NoIndex { |
+ _, err = cmpbin.WriteBytes(buf, p.Value().([]byte)) |
+ } else { |
+ _, err = cmpbin.WriteBytes(buf, p.Value().(ByteString)) |
+ } |
+ case PTTime: |
+ err = WriteTime(buf, p.Value().(time.Time)) |
+ case PTGeoPoint: |
+ err = WriteGeoPoint(buf, p.Value().(GeoPoint)) |
+ case PTKey: |
+ err = WriteKey(buf, context, p.Value().(Key)) |
+ case PTBlobKey: |
+ _, err = cmpbin.WriteString(buf, string(p.Value().(blobstore.Key))) |
+ } |
+ return |
+} |
+ |
+// ReadProperty reads a Property from the buffer. `context`, `appid`, and |
+// `namespace` behave the same way they do for ReadKey, but only have an |
+// effect if the decoded property has a Key value. |
+func ReadProperty(buf Buffer, context KeyContext, appid, namespace string) (p Property, err error) { |
+ val := interface{}(nil) |
+ typb, err := buf.ReadByte() |
+ if err != nil { |
+ return |
+ } |
+ is := ShouldIndex |
+ if (typb & 0x80) != 0 { |
+ is = NoIndex |
+ } |
+ switch PropertyType(typb & 0x7f) { |
+ case PTNull: |
+ case PTBoolTrue: |
+ val = true |
+ case PTBoolFalse: |
+ val = false |
+ case PTInt: |
+ val, _, err = cmpbin.ReadInt(buf) |
+ case PTFloat: |
+ val, _, err = cmpbin.ReadFloat64(buf) |
+ case PTString: |
+ val, _, err = cmpbin.ReadString(buf) |
+ case PTBytes: |
+ b := []byte(nil) |
+ if b, _, err = cmpbin.ReadBytes(buf); err != nil { |
+ break |
+ } |
+ if is == NoIndex { |
+ val = b |
+ } else { |
+ val = ByteString(b) |
+ } |
+ case PTTime: |
+ val, err = ReadTime(buf) |
+ case PTGeoPoint: |
+ val, err = ReadGeoPoint(buf) |
+ case PTKey: |
+ val, err = ReadKey(buf, context, appid, namespace) |
+ case PTBlobKey: |
+ s := "" |
+ if s, _, err = cmpbin.ReadString(buf); err != nil { |
+ break |
+ } |
+ val = blobstore.Key(s) |
+ default: |
+ err = fmt.Errorf("read: unknown type! %v", typb) |
+ } |
+ if err == nil { |
+ err = p.SetValue(val, is) |
+ } |
+ return |
+} |
+ |
+// WritePropertyMap writes an entire PropertyMap to the buffer. `context` |
+// behaves the same way that it does for WriteKey. If |
+// WritePropertyMapDeterministic is true, then the rows will be sorted by |
+// property name before they're serialized to buf (mostly useful for testing, |
+// but also potentially useful if you need to make a hash of the property data). |
+func WritePropertyMap(buf Buffer, propMap PropertyMap, context KeyContext) (err error) { |
+ defer recoverTo(&err) |
+ rows := make(sort.StringSlice, 0, len(propMap)) |
+ tmpBuf := &bytes.Buffer{} |
+ for name, vals := range propMap { |
+ tmpBuf.Reset() |
+ _, e := cmpbin.WriteString(tmpBuf, name) |
+ panicIf(e) |
+ _, e = cmpbin.WriteUint(tmpBuf, uint64(len(vals))) |
+ panicIf(e) |
+ for _, p := range vals { |
+ panicIf(WriteProperty(tmpBuf, p, context)) |
+ } |
+ rows = append(rows, tmpBuf.String()) |
+ } |
+ |
+ if WritePropertyMapDeterministic { |
+ rows.Sort() |
+ } |
+ |
+ _, e := cmpbin.WriteUint(buf, uint64(len(propMap))) |
+ panicIf(e) |
+ for _, r := range rows { |
+ _, e := buf.WriteString(r) |
+ panicIf(e) |
+ } |
+ return |
+} |
+ |
+// ReadPropertyMap reads a PropertyMap from the buffer. `context` and |
+// friends behave the same way that they do for ReadKey. |
+func ReadPropertyMap(buf Buffer, context KeyContext, appid, namespace string) (propMap PropertyMap, err error) { |
+ defer recoverTo(&err) |
+ |
+ numRows := uint64(0) |
+ numRows, _, e := cmpbin.ReadUint(buf) |
+ panicIf(e) |
+ if numRows > ReadPropertyMapReasonableLimit { |
+ err = fmt.Errorf("helper: tried to decode map with huge number of rows %d", numRows) |
+ return |
+ } |
+ |
+ name, prop := "", Property{} |
+ propMap = make(PropertyMap, numRows) |
+ for i := uint64(0); i < numRows; i++ { |
+ name, _, e = cmpbin.ReadString(buf) |
+ panicIf(e) |
+ |
+ numProps, _, e := cmpbin.ReadUint(buf) |
+ panicIf(e) |
+ if numProps > ReadPropertyMapReasonableLimit { |
+ err = fmt.Errorf("helper: tried to decode map with huge number of properties %d", numProps) |
+ return |
+ } |
+ props := make([]Property, 0, numProps) |
+ for j := uint64(0); j < numProps; j++ { |
+ prop, e = ReadProperty(buf, context, appid, namespace) |
+ panicIf(e) |
+ props = append(props, prop) |
+ } |
+ propMap[name] = props |
+ } |
+ return |
+} |
+ |
+type parseError error |
+ |
+func panicIf(err error) { |
+ if err != nil { |
+ panic(parseError(err)) |
+ } |
+} |
+ |
+func recoverTo(err *error) { |
+ if r := recover(); r != nil { |
+ if rerr := r.(parseError); rerr != nil { |
+ *err = error(rerr) |
+ } |
+ } |
+} |