Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(318)

Unified Diff: service/rawdatastore/serialize.go

Issue 1243323002: Refactor a bit. (Closed) Base URL: https://github.com/luci/gae.git@master
Patch Set: fix golint Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « service/rawdatastore/reflect.go ('k') | service/rawdatastore/serialize_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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)
+ }
+ }
+}
« no previous file with comments | « service/rawdatastore/reflect.go ('k') | service/rawdatastore/serialize_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698