package page import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "strconv" "time" "unicode/utf8" "uuid" ) const maxCursorBytes = 16384 // CursorError identifies malformed, version, sort, value, or size cursor failures via // Kind. It intentionally omits cursor payloads from its message. type CursorError struct{ Kind string } // Error implements error without including cursor contents. func (e *CursorError) Error() string { return "page: cursor " + e.Kind } // ValueError identifies an unsupported concrete type or invalid supported value // at Position in the row key, without exposing the value itself. type ValueError struct { Position int Type string } // Error implements error without including key values. func (e *ValueError) Error() string { return fmt.Sprintf("page: invalid key value at %d (%s)", e.Position, e.Type) } type envelope struct { Version int `json:"v"` Sort string `json:"sort"` Values [][2]string `json:"values"` } func encodeCursor(sortID string, values []any) (string, error) { e := envelope{Version: 1, Sort: sortID, Values: make([][2]string, len(values))} for i, v := range values { pair, ok := encodeValue(v) if !ok { return "", &ValueError{Position: i, Type: fmt.Sprintf("%T", v)} } e.Values[i] = pair } data, err := json.Marshal(e) if err != nil { return "", &CursorError{Kind: "malformed"} } if base64.RawURLEncoding.EncodedLen(len(data)) > maxCursorBytes { return "", &CursorError{Kind: "size"} } return base64.RawURLEncoding.EncodeToString(data), nil } func encodeValue(value any) ([2]string, bool) { var tag, payload string switch v := value.(type) { case int: tag, payload = "int", strconv.FormatInt(int64(v), 10) case int8: tag, payload = "int8", strconv.FormatInt(int64(v), 10) case int16: tag, payload = "int16", strconv.FormatInt(int64(v), 10) case int32: tag, payload = "int32", strconv.FormatInt(int64(v), 10) case int64: tag, payload = "int64", strconv.FormatInt(v, 10) case string: if !utf8.ValidString(v) { return [2]string{}, false } tag, payload = "string", v case time.Time: v = v.UTC() if v.Year() < 0 || v.Year() > 9999 { return [2]string{}, false } tag, payload = "time", v.Format(time.RFC3339Nano) case uuid.UUID: tag, payload = "uuid", v.String() default: return [2]string{}, false } return [2]string{tag, payload}, true } func decodeCursor(token string, sort Sort) ([]any, error) { failure := func(kind string) ([]any, error) { return nil, &CursorError{Kind: kind} } if len(token) > maxCursorBytes { return failure("size") } data, err := base64.RawURLEncoding.Strict().DecodeString(token) if err != nil || !utf8.Valid(data) || base64.RawURLEncoding.EncodeToString(data) != token { return failure("malformed") } // invariant: exactly three distinct envelope fields; struct decoding alone permits duplicates. decoder := json.NewDecoder(bytes.NewReader(data)) start, err := decoder.Token() if err != nil || start != json.Delim('{') { return failure("malformed") } fields := make(map[string]json.RawMessage, 3) for decoder.More() { name, err := decoder.Token() if err != nil { return failure("malformed") } key, ok := name.(string) if !ok || (key != "v" && key != "sort" && key != "values") { return failure("malformed") } if _, exists := fields[key]; exists { return failure("malformed") } var raw json.RawMessage if decoder.Decode(&raw) != nil { return failure("malformed") } fields[key] = raw } if _, err := decoder.Token(); err != nil || len(fields) != 3 { return failure("malformed") } if _, err := decoder.Token(); err != io.EOF { return failure("malformed") } var version int if bytes.Equal(fields["v"], []byte("null")) || json.Unmarshal(fields["v"], &version) != nil { return failure("malformed") } if version != 1 { return failure("version") } var id string if !jsonString(fields["sort"], &id) { return failure("malformed") } if id != sort.ID { return failure("sort") } var entries []json.RawMessage if len(fields["values"]) == 0 || fields["values"][0] != '[' || json.Unmarshal(fields["values"], &entries) != nil || len(entries) != len(sort.Columns) { return failure("malformed") } values := make([]any, len(entries)) for i, entry := range entries { var pair []json.RawMessage if len(entry) == 0 || entry[0] != '[' || json.Unmarshal(entry, &pair) != nil || len(pair) != 2 { return failure("malformed") } var tag, payload string if !jsonString(pair[0], &tag) || !jsonString(pair[1], &payload) { return failure("malformed") } value, ok := decodeValue(tag, payload) if !ok { return failure("value") } values[i] = value } return values, nil } func jsonString(raw json.RawMessage, value *string) bool { return len(raw) > 0 && raw[0] == '"' && json.Unmarshal(raw, value) == nil && utf8.ValidString(*value) } func decodeValue(tag, payload string) (any, bool) { switch tag { case "int", "int8", "int16", "int32", "int64": bits := strconv.IntSize switch tag { case "int8": bits = 8 case "int16": bits = 16 case "int32": bits = 32 case "int64": bits = 64 } n, err := strconv.ParseInt(payload, 10, bits) if err != nil || strconv.FormatInt(n, 10) != payload { return nil, false } switch tag { case "int": return int(n), true case "int8": return int8(n), true case "int16": return int16(n), true case "int32": return int32(n), true default: return n, true } case "string": return payload, utf8.ValidString(payload) case "time": t, err := time.Parse(time.RFC3339Nano, payload) t = t.UTC() return t, err == nil && t.Year() >= 0 && t.Year() <= 9999 case "uuid": id, err := uuid.Parse(payload) return id, err == nil && id.String() == payload default: return nil, false } }