Files
ucan/pkg/policy/selector/selector.go

343 lines
8.7 KiB
Go
Raw Normal View History

2024-08-19 23:16:36 +02:00
package selector
import (
"fmt"
"math"
"strconv"
2024-08-19 23:16:36 +02:00
"strings"
2024-08-20 15:55:04 +02:00
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
2024-09-12 18:19:50 +02:00
"github.com/ipld/go-ipld-prime/node/basicnode"
2024-08-19 23:16:36 +02:00
)
// Selector describes a UCAN policy selector, as specified here:
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
type Selector []segment
2024-08-20 15:55:04 +02:00
2024-10-15 17:26:49 +02:00
// Select perform the selection described by the selector on the input IPLD DAG.
// Select can return:
// - exactly one matched IPLD node
// - a resolutionerr error if not being able to resolve to a node
// - nil and no errors, if the selector couldn't match on an optional segment (with ?).
func (s Selector) Select(subject ipld.Node) (ipld.Node, error) {
2024-10-15 17:26:49 +02:00
return resolve(s, subject, nil)
}
2024-08-20 15:55:04 +02:00
func (s Selector) String() string {
var res strings.Builder
2024-08-20 15:55:04 +02:00
for _, seg := range s {
res.WriteString(seg.String())
2024-08-20 15:55:04 +02:00
}
return res.String()
2024-08-20 15:55:04 +02:00
}
type segment struct {
2024-08-19 23:16:36 +02:00
str string
identity bool
optional bool
2024-08-20 15:55:04 +02:00
iterator bool
slice []int64 // either 0-length or 2-length
2024-08-19 23:16:36 +02:00
field string
index int
}
// String returns the segment's string representation.
2024-08-20 15:55:04 +02:00
func (s segment) String() string {
return s.str
2024-08-19 23:16:36 +02:00
}
// Identity flags that this selector is the identity selector.
2024-08-20 15:55:04 +02:00
func (s segment) Identity() bool {
2024-08-19 23:16:36 +02:00
return s.identity
}
// Optional flags that this selector is optional.
2024-08-20 15:55:04 +02:00
func (s segment) Optional() bool {
return s.optional
2024-08-19 23:16:36 +02:00
}
// Iterator flags that this selector is an iterator segment.
2024-08-20 15:55:04 +02:00
func (s segment) Iterator() bool {
return s.iterator
2024-08-19 23:16:36 +02:00
}
// Slice flags that this segment targets a range of a slice.
func (s segment) Slice() []int64 {
2024-08-20 15:55:04 +02:00
return s.slice
2024-08-19 23:16:36 +02:00
}
// Field is the name of a field in a struct/map.
2024-08-20 15:55:04 +02:00
func (s segment) Field() string {
return s.field
}
2024-08-19 23:16:36 +02:00
// Index is an index of a slice.
2024-08-20 15:55:04 +02:00
func (s segment) Index() int {
return s.index
}
2024-08-19 23:16:36 +02:00
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, error) {
errIfNotOptional := func(s segment, err error) error {
if !s.Optional() {
return err
2024-09-13 14:44:26 +02:00
}
return nil
}
2024-08-20 15:55:04 +02:00
cur := subject
for _, seg := range sel {
2024-09-13 14:44:26 +02:00
// 1st level: handle the different segment types (iterator, field, slice, index)
// 2nd level: handle different node kinds (list, map, string, bytes)
switch {
case seg.Identity():
continue
2024-09-13 14:44:26 +02:00
case seg.Iterator():
switch {
case cur == nil || cur.Kind() == datamodel.Kind_Null:
2024-09-13 14:44:26 +02:00
if seg.Optional() {
// build an empty list
n, _ := qp.BuildList(basicnode.Prototype.Any, 0, func(_ datamodel.ListAssembler) {})
return n, nil
2024-08-20 15:55:04 +02:00
}
return nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
2024-08-20 15:55:04 +02:00
case cur.Kind() == datamodel.Kind_List:
// iterators are no-op on list
continue
case cur.Kind() == datamodel.Kind_Map:
// iterators on maps collect the values
nd, err := qp.BuildList(basicnode.Prototype.Any, cur.Length(), func(l datamodel.ListAssembler) {
2024-09-13 14:44:26 +02:00
it := cur.MapIterator()
for !it.Done() {
2024-09-13 16:26:46 +02:00
_, v, err := it.Next()
2024-09-13 14:44:26 +02:00
if err != nil {
// recovered by BuildList
// Error is bubbled up, but should never occur as we already checked the type,
// and are using the iterator correctly.
// This is verified with fuzzing.
panic(err)
2024-09-13 14:44:26 +02:00
}
qp.ListEntry(l, qp.Node(v))
2024-08-20 15:55:04 +02:00
}
})
if err != nil {
panic("should never happen")
}
return nd, nil
2024-09-13 14:44:26 +02:00
default:
return nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
2024-08-20 15:55:04 +02:00
}
2024-09-13 14:44:26 +02:00
case seg.Field() != "":
2024-08-20 15:55:04 +02:00
at = append(at, seg.Field())
switch {
case cur == nil:
err := newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
return nil, errIfNotOptional(seg, err)
case cur.Kind() == datamodel.Kind_Map:
n, err := cur.LookupByString(seg.Field())
if err != nil {
// the only possible error is missing field as we already check the type
2024-09-16 13:00:13 +02:00
if seg.Optional() {
cur = nil
} else {
return nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
2024-08-20 15:55:04 +02:00
}
} else {
cur = n
2024-08-20 15:55:04 +02:00
}
default:
err := newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
return nil, errIfNotOptional(seg, err)
2024-08-20 15:55:04 +02:00
}
2024-09-13 14:18:59 +02:00
case len(seg.Slice()) > 0:
2024-09-13 14:44:26 +02:00
if cur == nil {
err := newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
return nil, errIfNotOptional(seg, err)
}
2024-09-13 14:18:59 +02:00
slice := seg.Slice()
switch cur.Kind() {
case datamodel.Kind_List:
start, end := resolveSliceIndices(slice, cur.Length())
sliced, err := qp.BuildList(basicnode.Prototype.Any, end-start, func(l datamodel.ListAssembler) {
for i := start; i < end; i++ {
item, err := cur.LookupByIndex(i)
if err != nil {
// recovered by BuildList
// Error is bubbled up, but should never occur as we already checked the type and boundaries
// This is verified with fuzzing.
panic(err)
2024-09-13 14:49:52 +02:00
}
qp.ListEntry(l, qp.Node(item))
2024-09-13 14:18:59 +02:00
}
})
if err != nil {
panic("should never happen")
2024-09-13 14:18:59 +02:00
}
cur = sliced
case datamodel.Kind_Bytes:
b, _ := cur.AsBytes()
start, end := resolveSliceIndices(slice, int64(len(b)))
cur = basicnode.NewBytes(b[start:end])
case datamodel.Kind_String:
str, _ := cur.AsString()
runes := []rune(str)
start, end := resolveSliceIndices(slice, int64(len(runes)))
cur = basicnode.NewString(string(runes[start:end]))
default:
return nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
2024-08-20 15:55:04 +02:00
}
2024-09-13 14:44:26 +02:00
default: // Index()
at = append(at, strconv.Itoa(seg.Index()))
2024-09-13 14:44:26 +02:00
if cur == nil {
err := newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
return nil, errIfNotOptional(seg, err)
}
idx := seg.Index()
switch cur.Kind() {
case datamodel.Kind_List:
if idx < 0 {
idx = int(cur.Length()) + idx
}
if idx < 0 || idx >= int(cur.Length()) {
err := newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
return nil, errIfNotOptional(seg, err)
}
cur, _ = cur.LookupByIndex(int64(idx))
case datamodel.Kind_Bytes:
b, _ := cur.AsBytes()
if idx < 0 {
idx = len(b) + idx
}
if idx < 0 || idx >= len(b) {
2024-10-24 12:27:01 +02:00
err := newResolutionError(fmt.Sprintf("index %d out of bounds for bytes of length %d", seg.Index(), len(b)), at)
return nil, errIfNotOptional(seg, err)
}
cur = basicnode.NewInt(int64(b[idx]))
default:
return nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
2024-08-20 15:55:04 +02:00
}
}
}
// segment exhausted, we return where we are
return cur, nil
2024-08-20 15:55:04 +02:00
}
2024-09-13 14:44:26 +02:00
// resolveSliceIndices resolves the start and end indices for slicing a list or byte array.
//
// It takes the slice indices from the selector segment and the length of the list or byte array,
// and returns the resolved start and end indices. Negative indices are supported.
//
// Parameters:
// - slice: The slice indices from the selector segment.
// - length: The length of the list or byte array being sliced.
//
// Returns:
// - start: The resolved start index for slicing.
// - end: The resolved **excluded** end index for slicing.
func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
if len(slice) != 2 {
panic("should always be 2-length")
2024-09-13 14:44:26 +02:00
}
start, end = slice[0], slice[1]
// adjust boundaries
switch {
case slice[0] == math.MinInt:
start = 0
case slice[0] < 0:
2024-11-29 13:00:00 +01:00
// Check for potential overflow before adding
if -slice[0] > length {
start = 0
} else {
start = length + slice[0]
}
}
2024-11-29 13:00:00 +01:00
switch {
case slice[1] == math.MaxInt:
end = length
case slice[1] < 0:
2024-11-29 13:00:00 +01:00
// Check for potential overflow before adding
if -slice[1] > length {
end = 0
} else {
end = length + slice[1]
}
}
// backward iteration is not allowed, shortcut to an empty result
if start >= end {
start, end = 0, 0
2024-11-29 13:00:00 +01:00
return
}
2024-11-29 13:00:00 +01:00
// clamp out of bound
2024-10-24 12:27:01 +02:00
if start < 0 {
start = 0
}
2024-10-24 12:27:01 +02:00
if start > length {
start = length
2024-09-13 14:44:26 +02:00
}
2024-11-29 13:00:00 +01:00
if end < 0 {
end = 0
}
if end > length {
end = length
}
2024-11-29 13:00:00 +01:00
return
2024-09-13 14:44:26 +02:00
}
2024-08-20 15:55:04 +02:00
func kindString(n datamodel.Node) string {
if n == nil {
return "null"
}
return n.Kind().String()
}
type resolutionerr struct {
msg string
at []string
}
func (r resolutionerr) Name() string {
return "ResolutionError"
}
func (r resolutionerr) Message() string {
return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, "."))
}
func (r resolutionerr) At() []string {
return r.at
}
func (r resolutionerr) Error() string {
return r.Message()
}
func newResolutionError(message string, at []string) error {
2024-08-20 15:55:04 +02:00
return resolutionerr{message, at}
}