391 lines
9.4 KiB
Go
391 lines
9.4 KiB
Go
package selector
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/ipld/go-ipld-prime"
|
|
"github.com/ipld/go-ipld-prime/datamodel"
|
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
"github.com/ipld/go-ipld-prime/schema"
|
|
)
|
|
|
|
// Selector describes a UCAN policy selector, as specified here:
|
|
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
|
|
type Selector []segment
|
|
|
|
func (s Selector) String() string {
|
|
var res strings.Builder
|
|
for _, seg := range s {
|
|
res.WriteString(seg.String())
|
|
}
|
|
return res.String()
|
|
}
|
|
|
|
var Identity = segment{".", true, false, false, nil, "", 0}
|
|
|
|
var (
|
|
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
|
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
|
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
|
|
)
|
|
|
|
type segment struct {
|
|
str string
|
|
identity bool
|
|
optional bool
|
|
iterator bool
|
|
slice []int
|
|
field string
|
|
index int
|
|
}
|
|
|
|
// String returns the segment's string representation.
|
|
func (s segment) String() string {
|
|
return s.str
|
|
}
|
|
|
|
// Identity flags that this selector is the identity selector.
|
|
func (s segment) Identity() bool {
|
|
return s.identity
|
|
}
|
|
|
|
// Optional flags that this selector is optional.
|
|
func (s segment) Optional() bool {
|
|
return s.optional
|
|
}
|
|
|
|
// Iterator flags that this selector is an iterator segment.
|
|
func (s segment) Iterator() bool {
|
|
return s.iterator
|
|
}
|
|
|
|
// Slice flags that this segment targets a range of a slice.
|
|
func (s segment) Slice() []int {
|
|
return s.slice
|
|
}
|
|
|
|
// Field is the name of a field in a struct/map.
|
|
func (s segment) Field() string {
|
|
return s.field
|
|
}
|
|
|
|
// Index is an index of a slice.
|
|
func (s segment) Index() int {
|
|
return s.index
|
|
}
|
|
|
|
// Select uses a selector to extract an IPLD node or set of nodes from the
|
|
// passed subject node.
|
|
func Select(sel Selector, subject ipld.Node) (ipld.Node, []ipld.Node, error) {
|
|
return resolve(sel, subject, nil)
|
|
}
|
|
|
|
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) {
|
|
cur := subject
|
|
for i, seg := range sel {
|
|
if seg.Identity() {
|
|
continue
|
|
}
|
|
|
|
// 1st level: handle the different segment types (iterator, field, slice, index)
|
|
// 2nd level: handle different node kinds (list, map, string, bytes)
|
|
switch {
|
|
case seg.Iterator():
|
|
if cur == nil || cur.Kind() == datamodel.Kind_Null {
|
|
if seg.Optional() {
|
|
// build empty list
|
|
nb := basicnode.Prototype.List.NewBuilder()
|
|
assembler, err := nb.BeginList(0)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if err = assembler.Finish(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return nb.Build(), nil, nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
|
}
|
|
} else {
|
|
var many []ipld.Node
|
|
switch cur.Kind() {
|
|
case datamodel.Kind_List:
|
|
it := cur.ListIterator()
|
|
for !it.Done() {
|
|
k, v, err := it.Next()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
key := fmt.Sprintf("%d", k)
|
|
o, m, err := resolve(sel[i+1:], v, append(at[:], key))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if m != nil {
|
|
many = append(many, m...)
|
|
} else {
|
|
many = append(many, o)
|
|
}
|
|
}
|
|
case datamodel.Kind_Map:
|
|
it := cur.MapIterator()
|
|
for !it.Done() {
|
|
k, v, err := it.Next()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
key, _ := k.AsString()
|
|
o, m, err := resolve(sel[i+1:], v, append(at[:], key))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if m != nil {
|
|
many = append(many, m...)
|
|
} else {
|
|
many = append(many, o)
|
|
}
|
|
}
|
|
default:
|
|
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
|
}
|
|
|
|
return nil, many, nil
|
|
}
|
|
|
|
case seg.Field() != "":
|
|
at = append(at, seg.Field())
|
|
if cur == nil || cur.Kind() != datamodel.Kind_Map {
|
|
if seg.Optional() {
|
|
cur = nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
|
}
|
|
} else {
|
|
n, err := cur.LookupByString(seg.Field())
|
|
if err != nil {
|
|
if isMissing(err) {
|
|
if seg.Optional() {
|
|
cur = nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
|
|
}
|
|
} else {
|
|
return nil, nil, err
|
|
}
|
|
} else {
|
|
cur = n
|
|
}
|
|
}
|
|
|
|
case seg.Slice() != nil:
|
|
if cur == nil {
|
|
if seg.Optional() {
|
|
cur = nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
|
|
}
|
|
} else {
|
|
slice := seg.Slice()
|
|
var start, end int64
|
|
switch cur.Kind() {
|
|
case datamodel.Kind_List:
|
|
start, end = resolveSliceIndices(slice, cur.Length())
|
|
case datamodel.Kind_Bytes:
|
|
b, _ := cur.AsBytes()
|
|
start, end = resolveSliceIndices(slice, int64(len(b)))
|
|
case datamodel.Kind_String:
|
|
str, _ := cur.AsString()
|
|
start, end = resolveSliceIndices(slice, int64(len(str)))
|
|
default:
|
|
return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
|
|
}
|
|
|
|
if start < 0 || end < start {
|
|
if seg.Optional() {
|
|
cur = nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("slice out of bounds: [%d:%d]", start, end), at)
|
|
}
|
|
} else {
|
|
switch cur.Kind() {
|
|
case datamodel.Kind_List:
|
|
if end > cur.Length() {
|
|
end = cur.Length()
|
|
}
|
|
nb := basicnode.Prototype.List.NewBuilder()
|
|
assembler, _ := nb.BeginList(int64(end - start))
|
|
for i := start; i < end; i++ {
|
|
item, _ := cur.LookupByIndex(int64(i))
|
|
assembler.AssembleValue().AssignNode(item)
|
|
}
|
|
assembler.Finish()
|
|
cur = nb.Build()
|
|
case datamodel.Kind_Bytes:
|
|
b, _ := cur.AsBytes()
|
|
l := int64(len(b))
|
|
if end > l {
|
|
end = l
|
|
}
|
|
cur = basicnode.NewBytes(b[start:end])
|
|
case datamodel.Kind_String:
|
|
str, _ := cur.AsString()
|
|
l := int64(len(str))
|
|
if end > l {
|
|
end = l
|
|
}
|
|
cur = basicnode.NewString(str[start:end])
|
|
}
|
|
}
|
|
}
|
|
|
|
default:
|
|
at = append(at, fmt.Sprintf("%d", seg.Index()))
|
|
if cur == nil {
|
|
if seg.Optional() {
|
|
cur = nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
|
|
}
|
|
} else {
|
|
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()) {
|
|
if seg.Optional() {
|
|
cur = nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
|
}
|
|
} else {
|
|
cur, _ = cur.LookupByIndex(int64(idx))
|
|
}
|
|
case datamodel.Kind_String:
|
|
str, _ := cur.AsString()
|
|
if idx < 0 {
|
|
idx = len(str) + idx
|
|
}
|
|
if idx < 0 || idx >= len(str) {
|
|
if seg.Optional() {
|
|
cur = nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
|
}
|
|
} else {
|
|
cur = basicnode.NewString(string(str[idx]))
|
|
}
|
|
case datamodel.Kind_Bytes:
|
|
b, _ := cur.AsBytes()
|
|
if idx < 0 {
|
|
idx = len(b) + idx
|
|
}
|
|
if idx < 0 || idx >= len(b) {
|
|
if seg.Optional() {
|
|
cur = nil
|
|
} else {
|
|
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
|
}
|
|
} else {
|
|
cur = basicnode.NewInt(int64(b[idx]))
|
|
}
|
|
default:
|
|
return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return cur, nil, nil
|
|
}
|
|
|
|
// 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 end index for slicing.
|
|
func resolveSliceIndices(slice []int, length int64) (int64, int64) {
|
|
start, end := int64(0), length
|
|
if len(slice) > 0 {
|
|
start = int64(slice[0])
|
|
if start < 0 {
|
|
start = length + start
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
}
|
|
}
|
|
if len(slice) > 1 {
|
|
end = int64(slice[1])
|
|
if end <= 0 {
|
|
end = length + end
|
|
if end < start {
|
|
end = start
|
|
}
|
|
}
|
|
}
|
|
|
|
return start, end
|
|
}
|
|
|
|
func kindString(n datamodel.Node) string {
|
|
if n == nil {
|
|
return "null"
|
|
}
|
|
return n.Kind().String()
|
|
}
|
|
|
|
func isMissing(err error) bool {
|
|
if _, ok := err.(datamodel.ErrNotExists); ok {
|
|
return true
|
|
}
|
|
if _, ok := err.(schema.ErrNoSuchField); ok {
|
|
return true
|
|
}
|
|
if _, ok := err.(schema.ErrInvalidKey); ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
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 {
|
|
return resolutionerr{message, at}
|
|
}
|