package research import ( "bytes" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "io" "strconv" "strings" ) // Cursors locate a public corpus row, not an account or authentication token. // Binding to normalized filters prevents accidentally paging a different query. type position struct { Version int `json:"v"` ID int64 `json:"id"` Key string `json:"key"` Before bool `json:"before"` Filter string `json:"filter"` } var errCursor = errors.New("invalid cursor") func filterKey(f Filters) string { sort, search := f.Sort, f.SearchIn if sort == "" { sort = "recent" } if search == "" { search = "text" } data, _ := json.Marshal([]string{strings.TrimSpace(f.Query), f.Topic, sort, search, strconv.Itoa(f.PageSize)}) sum := sha256.Sum256(data) return hex.EncodeToString(sum[:16]) } func encodeCursor(f Filters, p position, before bool) string { p.Version, p.Before, p.Filter = 1, before, filterKey(f) if f.Sort != "title" { p.Key = "" } data, _ := json.Marshal(p) return base64.RawURLEncoding.EncodeToString(data) } func decodeCursor(f Filters) (position, error) { var p position if f.Cursor == "" { return p, nil } if len(f.Cursor) > 32768 { return p, errCursor } data, err := base64.RawURLEncoding.Strict().DecodeString(f.Cursor) if err != nil { return p, errCursor } decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() if decoder.Decode(&p) != nil || decoder.Decode(new(any)) != io.EOF || p.Version != 1 || p.ID <= 0 || p.Filter != filterKey(f) || strings.ContainsRune(p.Key, 0) || (f.Sort != "title" && p.Key != "") { return position{}, errCursor } return p, nil }