Compare commits
1 Commits
main
...
invocation
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fa3521007 |
20
.github/Repo.toml
vendored
20
.github/Repo.toml
vendored
@@ -1,20 +0,0 @@
|
|||||||
[scopes]
|
|
||||||
ci = [".github/workflows"]
|
|
||||||
config = [".github", "go.mod", "go.sum"]
|
|
||||||
docs = ["Readme.md", "LICENSE.md"]
|
|
||||||
assets = ["assets"]
|
|
||||||
pkg = ["pkg"]
|
|
||||||
args = ["pkg/args"]
|
|
||||||
command = ["pkg/command"]
|
|
||||||
container = ["pkg/container"]
|
|
||||||
meta = ["pkg/meta"]
|
|
||||||
policy = ["pkg/policy"]
|
|
||||||
secretbox = ["pkg/secretbox"]
|
|
||||||
token = ["token"]
|
|
||||||
delegation = ["token/delegation"]
|
|
||||||
invocation = ["token/invocation"]
|
|
||||||
toolkit = ["toolkit"]
|
|
||||||
examples = ["toolkit/_example"]
|
|
||||||
client = ["toolkit/client"]
|
|
||||||
issuer = ["toolkit/issuer"]
|
|
||||||
server = ["toolkit/server"]
|
|
||||||
34
.github/workflows/bench.yml
vendored
34
.github/workflows/bench.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: go continuous benchmark
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
deployments: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
benchmark:
|
|
||||||
name: Run Go continuous benchmark
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "stable"
|
|
||||||
- name: Run benchmark
|
|
||||||
run: go test -v ./... -bench=. -run=xxx -benchmem | tee output.txt
|
|
||||||
|
|
||||||
- name: Store benchmark result
|
|
||||||
uses: benchmark-action/github-action-benchmark@v1
|
|
||||||
with:
|
|
||||||
name: Go Benchmark
|
|
||||||
tool: 'go'
|
|
||||||
output-file-path: output.txt
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# Push and deploy GitHub pages branch automatically
|
|
||||||
auto-push: true
|
|
||||||
# Show alert with commit comment on detecting possible performance regression
|
|
||||||
alert-threshold: '200%'
|
|
||||||
comment-on-alert: true
|
|
||||||
8
.github/workflows/gotest.yml
vendored
8
.github/workflows/gotest.yml
vendored
@@ -7,14 +7,14 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "ubuntu" ]
|
os: [ "ubuntu" ]
|
||||||
go: [ "1.22.x", "1.23.x", ]
|
go: [ "1.21.x", "1.22.x", "1.23.x", ]
|
||||||
env:
|
env:
|
||||||
COVERAGES: ""
|
COVERAGES: ""
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
name: ${{ matrix.os}} (go ${{ matrix.go }})
|
name: ${{ matrix.os}} (go ${{ matrix.go }})
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- name: Go information
|
- name: Go information
|
||||||
@@ -22,6 +22,6 @@ jobs:
|
|||||||
go version
|
go version
|
||||||
go env
|
go env
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test -v ./... -tags jwx_es256k
|
run: go test -v ./...
|
||||||
- name: Check formatted
|
- name: Check formatted
|
||||||
run: gofmt -l .
|
run: gofmt -l .
|
||||||
@@ -29,7 +29,7 @@ Verbatim copies of both licenses are included below:
|
|||||||
```
|
```
|
||||||
Apache License
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
https://www.apache.org/licenses/
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
|||||||
76
Readme.md
76
Readme.md
@@ -1,76 +0,0 @@
|
|||||||
<div align="center">
|
|
||||||
<a href="https://code.sonr.org/go/ucan" target="_blank">
|
|
||||||
<img src="https://raw.githubusercontent.com/ucan-wg/go-ucan/v1/assets/logo.png" alt="go-ucan Logo" height="250"></img>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<h1 align="center">go-ucan</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<img src="https://img.shields.io/badge/UCAN-v1.0.0--rc.1-blue" alt="UCAN v1.0.0-rc.1">
|
|
||||||
<a href="https://code.sonr.org/go/ucan/tags">
|
|
||||||
<img alt="GitHub Tag" src="https://img.shields.io/github/v/tag/ucan-wg/go-ucan">
|
|
||||||
</a>
|
|
||||||
<a href="https://code.sonr.org/go/ucan/actions?query=">
|
|
||||||
<img src="https://code.sonr.org/go/ucan/actions/workflows/gotest.yml/badge.svg" alt="Build Status">
|
|
||||||
</a>
|
|
||||||
<a href="https://ucan-wg.github.io/go-ucan/dev/bench/">
|
|
||||||
<img alt="Go benchmarks" src="https://img.shields.io/badge/Benchmarks-go-blue">
|
|
||||||
</a>
|
|
||||||
<a href="https://code.sonr.org/go/ucan/blob/v1/LICENSE.md">
|
|
||||||
<img alt="Apache 2.0 OR MIT License" src="https://img.shields.io/badge/License-Apache--2.0_OR_MIT-green">
|
|
||||||
</a>
|
|
||||||
<a href="https://pkg.go.dev/code.sonr.org/go/ucan">
|
|
||||||
<img src="https://img.shields.io/badge/Docs-godoc-blue" alt="Docs">
|
|
||||||
</a>
|
|
||||||
<a href="https://discord.gg/JSyFG6XgVM">
|
|
||||||
<img src="https://img.shields.io/static/v1?label=Discord&message=join%20us!&color=mediumslateblue" alt="Discord">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
This is a go library to help the next generation of web and decentralized applications make use
|
|
||||||
of UCANs in their authorization flows.
|
|
||||||
|
|
||||||
User Controlled Authorization Networks (UCANs) are a way of doing authorization where users are fully in control. OAuth is designed for a centralized world, UCAN is the distributed user controlled version.
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
### Specifications
|
|
||||||
|
|
||||||
The UCAN specification is separated in multiple sub-spec:
|
|
||||||
- [Main specification](https://github.com/ucan-wg/spec)
|
|
||||||
- [Delegation](https://github.com/ucan-wg/delegation/tree/v1_ipld)
|
|
||||||
- [Invocation](https://github.com/ucan-wg/invocation)
|
|
||||||
- [Container](https://github.com/ucan-wg/container)
|
|
||||||
|
|
||||||
Not implemented yet:
|
|
||||||
- [Revocation](https://github.com/ucan-wg/revocation/tree/first-draft)
|
|
||||||
- [Promise](https://github.com/ucan-wg/promise/tree/v1-rc1)
|
|
||||||
|
|
||||||
### Talks
|
|
||||||
|
|
||||||
- [Decentralizing Auth, and UCAN Too - Brooklyn Zelenka (2023)](https://www.youtube.com/watch?v=MuHfrqw9gQA)
|
|
||||||
- [What's New in UCAN 1.0 - Brooklyn Zelenka (2024)](https://www.youtube.com/watch?v=-uohQzZcwF4)
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
`go-ucan` currently support the required parts of the UCAN specification: the main specification, delegation and invocation. It leverages the sibling project [`go-did-it`](https://code.sonr.org/go/did-it) for easy and extensible DID support.
|
|
||||||
|
|
||||||
Besides that, `go-ucan` also includes:
|
|
||||||
- support for encrypted values in token's metadata
|
|
||||||
|
|
||||||
## Getting Help
|
|
||||||
|
|
||||||
For usage questions, usecases, or issues reach out to us in our `go-ucan`
|
|
||||||
[Discord channel](https://discord.gg/3EHEQ6M8BC).
|
|
||||||
|
|
||||||
We would be happy to try to answer your question or try opening a new issue on
|
|
||||||
Github.
|
|
||||||
|
|
||||||
## UCAN Gopher
|
|
||||||
|
|
||||||
Artwork by [Bruno Monts](https://www.instagram.com/bruno_monts). Thank you [Renee French](http://reneefrench.blogspot.com/) for creating the [Go Gopher](https://blog.golang.org/gopher)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the dual license [Apache 2.0 OR MIT](https://code.sonr.org/go/ucan/blob/v1/LICENSE.md).
|
|
||||||
BIN
assets/logo.png
BIN
assets/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 125 KiB |
142
capability/command/command.go
Normal file
142
capability/command/command.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
separator = "/"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNew indicates that the wrapped error was encountered while creating
|
||||||
|
// a new Command.
|
||||||
|
var ErrNew = errors.New("failed to create Command from elems")
|
||||||
|
|
||||||
|
// ErrParse indicates that the wrapped error was encountered while
|
||||||
|
// attempting to parse a string as a Command.
|
||||||
|
var ErrParse = errors.New("failed to parse Command")
|
||||||
|
|
||||||
|
// ErrorJoin indicates that the wrapped error was encountered while
|
||||||
|
// attempting to join a new segment to a Command.
|
||||||
|
var ErrJoin = errors.New("failed to join segments to Command")
|
||||||
|
|
||||||
|
// ErrRequiresLeadingSlash is returned when a parsing a string that
|
||||||
|
// doesn't start with a [leading slash character].
|
||||||
|
//
|
||||||
|
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrRequiresLeadingSlash = parseError("a command requires a leading slash character")
|
||||||
|
|
||||||
|
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
|
||||||
|
// with a trailing slash character].
|
||||||
|
//
|
||||||
|
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrDisallowsTrailingSlash = parseError("a command must not include a trailing slash")
|
||||||
|
|
||||||
|
// ErrUCANNamespaceReserved is returned to indicate that a Command's
|
||||||
|
// first segment would contain the [reserved "ucan" namespace].
|
||||||
|
//
|
||||||
|
// [reserved "ucan" namespace]: https://github.com/ucan-wg/spec#ucan-namespace
|
||||||
|
var ErrUCANNamespaceReserved = errors.New("the UCAN namespace is reserved")
|
||||||
|
|
||||||
|
// ErrRequiresLowercase is returned if a Command contains, or would contain,
|
||||||
|
// [uppercase unicode characters].
|
||||||
|
//
|
||||||
|
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrRequiresLowercase = parseError("UCAN path segments must must not contain upper-case characters")
|
||||||
|
|
||||||
|
func parseError(msg string) error {
|
||||||
|
return fmt.Errorf("%w: %s", ErrParse, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fmt.Stringer = (*Command)(nil)
|
||||||
|
|
||||||
|
// Command is a concrete messages (a "verb") that MUST be unambiguously
|
||||||
|
// interpretable by the Subject of a UCAN.
|
||||||
|
//
|
||||||
|
// A [Command] is composed of a leading slash which is optionally followed
|
||||||
|
// by one or more slash-separated Segments of lowercase characters.
|
||||||
|
//
|
||||||
|
// [Command]: https://github.com/ucan-wg/spec#command
|
||||||
|
type Command struct {
|
||||||
|
segments []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a validated command from the provided list of segment
|
||||||
|
// strings. An error is returned if an invalid Command would be
|
||||||
|
// formed
|
||||||
|
func New(segments ...string) (*Command, error) {
|
||||||
|
return newCommand(ErrNew, segments...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommand(err error, segments ...string) (*Command, error) {
|
||||||
|
if len(segments) > 0 && segments[0] == "ucan" {
|
||||||
|
return nil, fmt.Errorf("%w: %w", err, ErrUCANNamespaceReserved)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := Command{segments}
|
||||||
|
|
||||||
|
return &cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse verifies that the provided string contains the required
|
||||||
|
// [segment structure] and, if valid, returns the resulting
|
||||||
|
// Command.
|
||||||
|
//
|
||||||
|
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
func Parse(s string) (*Command, error) {
|
||||||
|
if !strings.HasPrefix(s, "/") {
|
||||||
|
return nil, ErrRequiresLeadingSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
||||||
|
return nil, ErrDisallowsTrailingSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
if s != strings.ToLower(s) {
|
||||||
|
return nil, ErrRequiresLowercase
|
||||||
|
}
|
||||||
|
|
||||||
|
// The leading slash will result in the first element from strings.Split
|
||||||
|
// being an empty string which is removed as strings.Join will ignore it.
|
||||||
|
return newCommand(ErrParse, strings.Split(s, "/")[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Top] is the most powerful capability.
|
||||||
|
//
|
||||||
|
// This function returns a Command that is a wildcard and therefore represents the
|
||||||
|
// most powerful abilily. As such it should be handle with care and used
|
||||||
|
// sparingly.
|
||||||
|
//
|
||||||
|
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
||||||
|
func Top() *Command {
|
||||||
|
cmd, _ := New()
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns true if the provided string is a valid UCAN command.
|
||||||
|
func IsValid(s string) bool {
|
||||||
|
_, err := Parse(s)
|
||||||
|
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join appends segments to the end of this command using the required
|
||||||
|
// segment separator.
|
||||||
|
func (c *Command) Join(segments ...string) (*Command, error) {
|
||||||
|
return newCommand(ErrJoin, append(c.segments, segments...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segments returns the ordered segments that comprise the Command as a
|
||||||
|
// slice of strings.
|
||||||
|
func (c *Command) Segments() []string {
|
||||||
|
return c.segments
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the composed representation the command. This is also
|
||||||
|
// the required wire representation (before IPLD encoding occurs.)
|
||||||
|
func (c *Command) String() string {
|
||||||
|
return "/" + strings.Join([]string(c.segments), "/")
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/command"
|
"github.com/ucan-wg/go-ucan/v1/capability/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTop(t *testing.T) {
|
func TestTop(t *testing.T) {
|
||||||
@@ -13,81 +13,74 @@ func TestTop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidCommand(t *testing.T) {
|
func TestIsValidCommand(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("succeeds when", func(t *testing.T) {
|
t.Run("succeeds when", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, testcase := range validTestcases(t) {
|
for _, testcase := range validTestcases(t) {
|
||||||
|
testcase := testcase
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
require.True(t, command.IsValid(testcase.inp))
|
require.True(t, command.IsValid(testcase.inp))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fails when", func(t *testing.T) {
|
t.Run("fails when", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, testcase := range invalidTestcases(t) {
|
for _, testcase := range invalidTestcases(t) {
|
||||||
|
testcase := testcase
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
require.False(t, command.IsValid(testcase.inp))
|
require.False(t, command.IsValid(testcase.inp))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
require.Equal(t, command.Top(), command.New())
|
|
||||||
require.Equal(t, "/foo", command.New("foo").String())
|
|
||||||
require.Equal(t, "/foo/bar", command.New("foo", "bar").String())
|
|
||||||
require.Equal(t, "/foo/bar/baz", command.New("foo", "bar/baz").String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseCommand(t *testing.T) {
|
func TestParseCommand(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("succeeds when", func(t *testing.T) {
|
t.Run("succeeds when", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, testcase := range validTestcases(t) {
|
for _, testcase := range validTestcases(t) {
|
||||||
|
testcase := testcase
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
cmd, err := command.Parse("/elem0/elem1/elem2")
|
cmd, err := command.Parse("/elem0/elem1/elem2")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, cmd)
|
require.NotNil(t, cmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fails when", func(t *testing.T) {
|
t.Run("fails when", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, testcase := range invalidTestcases(t) {
|
for _, testcase := range invalidTestcases(t) {
|
||||||
|
testcase := testcase
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
cmd, err := command.Parse(testcase.inp)
|
cmd, err := command.Parse(testcase.inp)
|
||||||
|
require.ErrorIs(t, err, command.ErrParse)
|
||||||
require.ErrorIs(t, err, testcase.err)
|
require.ErrorIs(t, err, testcase.err)
|
||||||
require.Zero(t, cmd)
|
require.Nil(t, cmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEquality(t *testing.T) {
|
|
||||||
require.True(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/baz"))
|
|
||||||
require.False(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/bazz"))
|
|
||||||
require.False(t, command.MustParse("/foo/bar") == command.MustParse("/foo/bar/baz"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJoin(t *testing.T) {
|
|
||||||
require.Equal(t, "/foo", command.Top().Join("foo").String())
|
|
||||||
require.Equal(t, "/foo/bar", command.Top().Join("foo/bar").String())
|
|
||||||
require.Equal(t, "/foo/bar", command.Top().Join("foo", "bar").String())
|
|
||||||
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo/bar").String())
|
|
||||||
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo", "bar").String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSegments(t *testing.T) {
|
|
||||||
require.Empty(t, command.Top().Segments())
|
|
||||||
require.Equal(t, []string{"foo", "bar", "baz"}, command.MustParse("/foo/bar/baz").Segments())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCovers(t *testing.T) {
|
|
||||||
require.True(t, command.MustParse("/foo/bar/baz").Covers(command.MustParse("/foo/bar/baz")))
|
|
||||||
require.True(t, command.MustParse("/foo/bar").Covers(command.MustParse("/foo/bar/baz")))
|
|
||||||
require.False(t, command.MustParse("/foo/bar/baz").Covers(command.MustParse("/foo/bar")))
|
|
||||||
require.True(t, command.MustParse("/").Covers(command.MustParse("/foo")))
|
|
||||||
require.True(t, command.MustParse("/").Covers(command.MustParse("/foo/bar/baz")))
|
|
||||||
require.False(t, command.MustParse("/foo").Covers(command.MustParse("/foo00")))
|
|
||||||
require.False(t, command.MustParse("/foo/bar").Covers(command.MustParse("/foo/bar00")))
|
|
||||||
}
|
|
||||||
|
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
name string
|
name string
|
||||||
inp string
|
inp string
|
||||||
@@ -141,6 +134,20 @@ func invalidTestcases(t *testing.T) []errorTestcase {
|
|||||||
},
|
},
|
||||||
err: command.ErrDisallowsTrailingSlash,
|
err: command.ErrDisallowsTrailingSlash,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
testcase: testcase{
|
||||||
|
name: "only reserved ucan namespace",
|
||||||
|
inp: "/ucan",
|
||||||
|
},
|
||||||
|
err: command.ErrUCANNamespaceReserved,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testcase: testcase{
|
||||||
|
name: "reserved ucan namespace prefix",
|
||||||
|
inp: "/ucan/elem0/elem1/elem2",
|
||||||
|
},
|
||||||
|
err: command.ErrUCANNamespaceReserved,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
testcase: testcase{
|
testcase: testcase{
|
||||||
name: "uppercase character are present",
|
name: "uppercase character are present",
|
||||||
@@ -9,15 +9,10 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime/must"
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/limits"
|
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
||||||
"code.sonr.org/go/ucan/pkg/policy/selector"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func FromIPLD(node datamodel.Node) (Policy, error) {
|
func FromIPLD(node datamodel.Node) (Policy, error) {
|
||||||
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
|
|
||||||
return nil, fmt.Errorf("policy contains integer values outside safe bounds: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return statementsFromIPLD("/", node)
|
return statementsFromIPLD("/", node)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,14 +40,14 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
}
|
}
|
||||||
op := must.String(opNode)
|
op := must.String(opNode)
|
||||||
|
|
||||||
arg2AsSelector := func(op string) (selector.Selector, error) {
|
arg2AsSelector := func() (selector.Selector, error) {
|
||||||
nd, _ := node.LookupByIndex(1)
|
nd, _ := node.LookupByIndex(1)
|
||||||
if nd.Kind() != datamodel.Kind_String {
|
if nd.Kind() != datamodel.Kind_String {
|
||||||
return nil, ErrNotAString(combinePath(path, op, 1))
|
return nil, ErrNotAString(path + "1/")
|
||||||
}
|
}
|
||||||
sel, err := selector.Parse(must.String(nd))
|
sel, err := selector.Parse(must.String(nd))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidSelector(combinePath(path, op, 1), err)
|
return nil, ErrInvalidSelector(path+"1/", err)
|
||||||
}
|
}
|
||||||
return sel, nil
|
return sel, nil
|
||||||
}
|
}
|
||||||
@@ -62,15 +57,15 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
switch op {
|
switch op {
|
||||||
case KindNot:
|
case KindNot:
|
||||||
arg2, _ := node.LookupByIndex(1)
|
arg2, _ := node.LookupByIndex(1)
|
||||||
statement, err := statementFromIPLD(combinePath(path, op, 1), arg2)
|
statement, err := statementFromIPLD(path+"1/", arg2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return negation{statement: statement}, nil
|
return Not(statement), nil
|
||||||
|
|
||||||
case KindAnd, KindOr:
|
case KindAnd, KindOr:
|
||||||
arg2, _ := node.LookupByIndex(1)
|
arg2, _ := node.LookupByIndex(1)
|
||||||
statement, err := statementsFromIPLD(combinePath(path, op, 1), arg2)
|
statement, err := statementsFromIPLD(path+"1/", arg2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -81,8 +76,8 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
}
|
}
|
||||||
case 3:
|
case 3:
|
||||||
switch op {
|
switch op {
|
||||||
case KindEqual, KindNotEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
||||||
sel, err := arg2AsSelector(op)
|
sel, err := arg2AsSelector()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -90,31 +85,28 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
return equality{kind: op, selector: sel, value: arg3}, nil
|
return equality{kind: op, selector: sel, value: arg3}, nil
|
||||||
|
|
||||||
case KindLike:
|
case KindLike:
|
||||||
sel, err := arg2AsSelector(op)
|
sel, err := arg2AsSelector()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pattern, _ := node.LookupByIndex(2)
|
pattern, _ := node.LookupByIndex(2)
|
||||||
if pattern.Kind() != datamodel.Kind_String {
|
if pattern.Kind() != datamodel.Kind_String {
|
||||||
return nil, ErrNotAString(combinePath(path, op, 2))
|
return nil, ErrNotAString(path + "2/")
|
||||||
}
|
}
|
||||||
g, err := parseGlob(must.String(pattern))
|
res, err := Like(sel, must.String(pattern))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
|
return nil, ErrInvalidPattern(path+"2/", err)
|
||||||
}
|
}
|
||||||
return wildcard{selector: sel, pattern: g}, nil
|
return res, nil
|
||||||
|
|
||||||
case KindAll, KindAny:
|
case KindAll, KindAny:
|
||||||
sel, err := arg2AsSelector(op)
|
sel, err := arg2AsSelector()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
statementsNode, _ := node.LookupByIndex(2)
|
statementsNodes, _ := node.LookupByIndex(2)
|
||||||
statement, err := statementFromIPLD(combinePath(path, op, 1), statementsNode)
|
statements, err := statementsFromIPLD(path+"1/", statementsNodes)
|
||||||
if err != nil {
|
return quantifier{kind: op, selector: sel, statements: statements}, nil
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return quantifier{kind: op, selector: sel, statement: statement}, nil
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, ErrUnrecognizedOperator(path, op)
|
return nil, ErrUnrecognizedOperator(path, op)
|
||||||
@@ -131,7 +123,7 @@ func statementsFromIPLD(path string, node datamodel.Node) ([]Statement, error) {
|
|||||||
return nil, ErrNotATuple(path)
|
return nil, ErrNotATuple(path)
|
||||||
}
|
}
|
||||||
if node.Length() == 0 {
|
if node.Length() == 0 {
|
||||||
return nil, nil
|
return nil, ErrEmptyList(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]Statement, node.Length())
|
res := make([]Statement, node.Length())
|
||||||
@@ -237,7 +229,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = listBuilder.AssembleValue().AssignString(string(statement.pattern))
|
err = listBuilder.AssembleValue().AssignString(statement.pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -251,7 +243,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
args, err := statementToIPLD(statement.statement)
|
args, err := statementsToIPLD(statement.statements)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -268,7 +260,3 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
|
|
||||||
return list.Build(), nil
|
return list.Build(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func combinePath(prev string, operator string, index int) string {
|
|
||||||
return fmt.Sprintf("%s%d-%s/", prev, index, operator)
|
|
||||||
}
|
|
||||||
@@ -35,6 +35,10 @@ func ErrNotATuple(path string) error {
|
|||||||
return errWithPath{path: path, msg: "not a tuple"}
|
return errWithPath{path: path, msg: "not a tuple"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrEmptyList(path string) error {
|
||||||
|
return errWithPath{path: path, msg: "empty list"}
|
||||||
|
}
|
||||||
|
|
||||||
func safeStr(str string) string {
|
func safeStr(str string) string {
|
||||||
if len(str) > 10 {
|
if len(str) > 10 {
|
||||||
return str[:10]
|
return str[:10]
|
||||||
@@ -11,41 +11,22 @@ import (
|
|||||||
func TestIpldRoundTrip(t *testing.T) {
|
func TestIpldRoundTrip(t *testing.T) {
|
||||||
const illustrativeExample = `
|
const illustrativeExample = `
|
||||||
[
|
[
|
||||||
["==", ".status", "draft"],
|
["==", ".status", "draft"],
|
||||||
["all", ".reviewer", ["like", ".email", "*@example.com"]],
|
["all", ".reviewer", [
|
||||||
["any", ".tags",
|
["like", ".email", "*@example.com"]]
|
||||||
["or", [
|
],
|
||||||
["==", ".", "news"],
|
["any", ".tags", [
|
||||||
["==", ".", "press"]
|
["or", [
|
||||||
]]
|
["==", ".", "news"],
|
||||||
]
|
["==", ".", "press"]]
|
||||||
]`
|
]]
|
||||||
|
]
|
||||||
// must contain all the operators
|
|
||||||
const allOps = `
|
|
||||||
[
|
|
||||||
["and", [
|
|
||||||
["==", ".foo1", ".bar1"],
|
|
||||||
["!=", ".foo2", ".bar2"]
|
|
||||||
]],
|
|
||||||
["or", [
|
|
||||||
[">", ".foo5", 5.2],
|
|
||||||
[">=", ".foo6", 6.2]
|
|
||||||
]],
|
|
||||||
["not", ["like", ".foo7", "*@example.com"]],
|
|
||||||
["all", ".foo8",
|
|
||||||
["<", ".foo3", 3]
|
|
||||||
],
|
|
||||||
["any", ".foo9",
|
|
||||||
["<=", ".foo4", 4]
|
|
||||||
]
|
|
||||||
]`
|
]`
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name, dagJsonStr string
|
name, dagJsonStr string
|
||||||
}{
|
}{
|
||||||
{"illustrativeExample", illustrativeExample},
|
{"illustrativeExample", illustrativeExample},
|
||||||
{"allOps", allOps},
|
|
||||||
} {
|
} {
|
||||||
nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode)
|
nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
52
capability/policy/literal/literal.go
Normal file
52
capability/policy/literal/literal.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package literal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Node(n ipld.Node) ipld.Node {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func Link(cid ipld.Link) ipld.Node {
|
||||||
|
nb := basicnode.Prototype.Link.NewBuilder()
|
||||||
|
nb.AssignLink(cid)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bool(val bool) ipld.Node {
|
||||||
|
nb := basicnode.Prototype.Bool.NewBuilder()
|
||||||
|
nb.AssignBool(val)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int(val int64) ipld.Node {
|
||||||
|
nb := basicnode.Prototype.Int.NewBuilder()
|
||||||
|
nb.AssignInt(val)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Float(val float64) ipld.Node {
|
||||||
|
nb := basicnode.Prototype.Float.NewBuilder()
|
||||||
|
nb.AssignFloat(val)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func String(val string) ipld.Node {
|
||||||
|
nb := basicnode.Prototype.String.NewBuilder()
|
||||||
|
nb.AssignString(val)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bytes(val []byte) ipld.Node {
|
||||||
|
nb := basicnode.Prototype.Bytes.NewBuilder()
|
||||||
|
nb.AssignBytes(val)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Null() ipld.Node {
|
||||||
|
nb := basicnode.Prototype.Any.NewBuilder()
|
||||||
|
nb.AssignNull()
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
163
capability/policy/match.go
Normal file
163
capability/policy/match.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match determines if the IPLD node matches the policy document.
|
||||||
|
func Match(policy Policy, node ipld.Node) bool {
|
||||||
|
for _, stmt := range policy {
|
||||||
|
ok := matchStatement(stmt, node)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStatement(statement Statement, node ipld.Node) bool {
|
||||||
|
switch statement.Kind() {
|
||||||
|
case KindEqual:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return datamodel.DeepEqual(s.value, one)
|
||||||
|
}
|
||||||
|
case KindGreaterThan:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isOrdered(s.value, one, gt)
|
||||||
|
}
|
||||||
|
case KindGreaterThanOrEqual:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isOrdered(s.value, one, gte)
|
||||||
|
}
|
||||||
|
case KindLessThan:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isOrdered(s.value, one, lt)
|
||||||
|
}
|
||||||
|
case KindLessThanOrEqual:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isOrdered(s.value, one, lte)
|
||||||
|
}
|
||||||
|
case KindNot:
|
||||||
|
if s, ok := statement.(negation); ok {
|
||||||
|
return !matchStatement(s.statement, node)
|
||||||
|
}
|
||||||
|
case KindAnd:
|
||||||
|
if s, ok := statement.(connective); ok {
|
||||||
|
for _, cs := range s.statements {
|
||||||
|
r := matchStatement(cs, node)
|
||||||
|
if !r {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case KindOr:
|
||||||
|
if s, ok := statement.(connective); ok {
|
||||||
|
if len(s.statements) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, cs := range s.statements {
|
||||||
|
r := matchStatement(cs, node)
|
||||||
|
if r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case KindLike:
|
||||||
|
if s, ok := statement.(wildcard); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v, err := one.AsString()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.glob.Match(v)
|
||||||
|
}
|
||||||
|
case KindAll:
|
||||||
|
if s, ok := statement.(quantifier); ok {
|
||||||
|
_, many, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || many == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, n := range many {
|
||||||
|
ok := Match(s.statements, n)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case KindAny:
|
||||||
|
if s, ok := statement.(quantifier); ok {
|
||||||
|
_, many, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || many == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, n := range many {
|
||||||
|
ok := Match(s.statements, n)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
||||||
|
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
||||||
|
a := must.Int(actual)
|
||||||
|
b := must.Int(expected)
|
||||||
|
return satisfies(cmp.Compare(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float {
|
||||||
|
a, err := actual.AsFloat()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("extracting node float: %w", err))
|
||||||
|
}
|
||||||
|
b, err := expected.AsFloat()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("extracting selector float: %w", err))
|
||||||
|
}
|
||||||
|
return satisfies(cmp.Compare(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func gt(order int) bool { return order == 1 }
|
||||||
|
func gte(order int) bool { return order == 0 || order == 1 }
|
||||||
|
func lt(order int) bool { return order == -1 }
|
||||||
|
func lte(order int) bool { return order == 0 || order == -1 }
|
||||||
420
capability/policy/match_test.go
Normal file
420
capability/policy/match_test.go
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/policy/literal"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatch(t *testing.T) {
|
||||||
|
t.Run("equality", func(t *testing.T) {
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.String
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignString("test")
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{Equal(selector.MustParse("."), literal.String("test"))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse("."), literal.Int(138))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("int", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{Equal(selector.MustParse("."), literal.Int(138))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("float", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Float
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignFloat(1.138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("IPLD Link", func(t *testing.T) {
|
||||||
|
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
||||||
|
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
||||||
|
|
||||||
|
np := basicnode.Prototype.Link
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignLink(l0)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string in map", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Map
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
ma, _ := nb.BeginMap(1)
|
||||||
|
ma.AssembleKey().AssignString("foo")
|
||||||
|
ma.AssembleValue().AssignString("bar")
|
||||||
|
ma.Finish()
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string in list", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.List
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
la, _ := nb.BeginList(1)
|
||||||
|
la.AssembleValue().AssignString("foo")
|
||||||
|
la.Finish()
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inequality", func(t *testing.T) {
|
||||||
|
t.Run("gt int", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gte int", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gt float", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Float
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignFloat(1.38)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gte float", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Float
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignFloat(1.38)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("lt int", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("lte int", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negation", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Bool
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignBool(false)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("conjunction", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{
|
||||||
|
And(
|
||||||
|
GreaterThan(selector.MustParse("."), literal.Int(1)),
|
||||||
|
LessThan(selector.MustParse("."), literal.Int(1138)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{
|
||||||
|
And(
|
||||||
|
GreaterThan(selector.MustParse("."), literal.Int(1)),
|
||||||
|
Equal(selector.MustParse("."), literal.Int(1138)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{And()}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("disjunction", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{
|
||||||
|
Or(
|
||||||
|
GreaterThan(selector.MustParse("."), literal.Int(138)),
|
||||||
|
LessThan(selector.MustParse("."), literal.Int(1138)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{
|
||||||
|
Or(
|
||||||
|
GreaterThan(selector.MustParse("."), literal.Int(138)),
|
||||||
|
Equal(selector.MustParse("."), literal.Int(1138)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{Or()}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wildcard", func(t *testing.T) {
|
||||||
|
pattern := `Alice\*, Bob*, Carol.`
|
||||||
|
|
||||||
|
for _, s := range []string{
|
||||||
|
"Alice*, Bob, Carol.",
|
||||||
|
"Alice*, Bob, Dan, Erin, Carol.",
|
||||||
|
"Alice*, Bob , Carol.",
|
||||||
|
"Alice*, Bob*, Carol.",
|
||||||
|
} {
|
||||||
|
func(s string) {
|
||||||
|
t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.String
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignString(s)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
statement, err := Like(selector.MustParse("."), pattern)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pol := Policy{statement}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
}(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range []string{
|
||||||
|
"Alice*, Bob, Carol",
|
||||||
|
"Alice*, Bob*, Carol!",
|
||||||
|
"Alice Cooper, Bob, Carol.",
|
||||||
|
"Alice, Bob, Carol.",
|
||||||
|
" Alice*, Bob, Carol. ",
|
||||||
|
} {
|
||||||
|
func(s string) {
|
||||||
|
t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.String
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignString(s)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
statement, err := Like(selector.MustParse("."), pattern)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pol := Policy{statement}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
}(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quantification", func(t *testing.T) {
|
||||||
|
buildValueNode := func(v int64) ipld.Node {
|
||||||
|
np := basicnode.Prototype.Map
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
ma, _ := nb.BeginMap(1)
|
||||||
|
ma.AssembleKey().AssignString("value")
|
||||||
|
ma.AssembleValue().AssignInt(v)
|
||||||
|
ma.Finish()
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("all", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.List
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
la, _ := nb.BeginList(5)
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(5))
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(10))
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(20))
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(50))
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(100))
|
||||||
|
la.Finish()
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{
|
||||||
|
All(
|
||||||
|
selector.MustParse(".[]"),
|
||||||
|
GreaterThan(selector.MustParse(".value"), literal.Int(2)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{
|
||||||
|
All(
|
||||||
|
selector.MustParse(".[]"),
|
||||||
|
GreaterThan(selector.MustParse(".value"), literal.Int(20)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("any", func(t *testing.T) {
|
||||||
|
np := basicnode.Prototype.List
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
la, _ := nb.BeginList(5)
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(5))
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(10))
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(20))
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(50))
|
||||||
|
la.AssembleValue().AssignNode(buildValueNode(100))
|
||||||
|
la.Finish()
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
|
pol := Policy{
|
||||||
|
Any(
|
||||||
|
selector.MustParse(".[]"),
|
||||||
|
GreaterThan(selector.MustParse(".value"), literal.Int(10)),
|
||||||
|
LessThan(selector.MustParse(".value"), literal.Int(50)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok := Match(pol, nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
pol = Policy{
|
||||||
|
Any(
|
||||||
|
selector.MustParse(".[]"),
|
||||||
|
GreaterThan(selector.MustParse(".value"), literal.Int(100)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok = Match(pol, nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
125
capability/policy/policy.go
Normal file
125
capability/policy/policy.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gobwas/glob"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KindEqual = "==" // implemented by equality
|
||||||
|
KindGreaterThan = ">" // implemented by equality
|
||||||
|
KindGreaterThanOrEqual = ">=" // implemented by equality
|
||||||
|
KindLessThan = "<" // implemented by equality
|
||||||
|
KindLessThanOrEqual = "<=" // implemented by equality
|
||||||
|
KindNot = "not" // implemented by negation
|
||||||
|
KindAnd = "and" // implemented by connective
|
||||||
|
KindOr = "or" // implemented by connective
|
||||||
|
KindLike = "like" // implemented by wildcard
|
||||||
|
KindAll = "all" // implemented by quantifier
|
||||||
|
KindAny = "any" // implemented by quantifier
|
||||||
|
)
|
||||||
|
|
||||||
|
type Policy []Statement
|
||||||
|
|
||||||
|
type Statement interface {
|
||||||
|
Kind() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type equality struct {
|
||||||
|
kind string
|
||||||
|
selector selector.Selector
|
||||||
|
value ipld.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e equality) Kind() string {
|
||||||
|
return e.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func Equal(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{KindEqual, selector, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{KindGreaterThan, selector, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{KindGreaterThanOrEqual, selector, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LessThan(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{KindLessThan, selector, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{KindLessThanOrEqual, selector, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
type negation struct {
|
||||||
|
statement Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n negation) Kind() string {
|
||||||
|
return KindNot
|
||||||
|
}
|
||||||
|
|
||||||
|
func Not(stmt Statement) Statement {
|
||||||
|
return negation{stmt}
|
||||||
|
}
|
||||||
|
|
||||||
|
type connective struct {
|
||||||
|
kind string
|
||||||
|
statements []Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c connective) Kind() string {
|
||||||
|
return c.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func And(stmts ...Statement) Statement {
|
||||||
|
return connective{KindAnd, stmts}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Or(stmts ...Statement) Statement {
|
||||||
|
return connective{KindOr, stmts}
|
||||||
|
}
|
||||||
|
|
||||||
|
type wildcard struct {
|
||||||
|
selector selector.Selector
|
||||||
|
pattern string
|
||||||
|
glob glob.Glob // not serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n wildcard) Kind() string {
|
||||||
|
return KindLike
|
||||||
|
}
|
||||||
|
|
||||||
|
func Like(selector selector.Selector, pattern string) (Statement, error) {
|
||||||
|
g, err := glob.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return wildcard{selector, pattern, g}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type quantifier struct {
|
||||||
|
kind string
|
||||||
|
selector selector.Selector
|
||||||
|
statements []Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n quantifier) Kind() string {
|
||||||
|
return n.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func All(selector selector.Selector, policy ...Statement) Statement {
|
||||||
|
return quantifier{KindAll, selector, policy}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Any(selector selector.Selector, policy ...Statement) Statement {
|
||||||
|
return quantifier{KindAny, selector, policy}
|
||||||
|
}
|
||||||
160
capability/policy/selector/parsing.go
Normal file
160
capability/policy/selector/parsing.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Parse(str string) (Selector, error) {
|
||||||
|
if len(str) == 0 {
|
||||||
|
return nil, newParseError("empty selector", str, 0, "")
|
||||||
|
}
|
||||||
|
if string(str[0]) != "." {
|
||||||
|
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
col := 0
|
||||||
|
var sel Selector
|
||||||
|
for _, tok := range tokenize(str) {
|
||||||
|
seg := tok
|
||||||
|
opt := strings.HasSuffix(tok, "?")
|
||||||
|
if opt {
|
||||||
|
seg = tok[0 : len(tok)-1]
|
||||||
|
}
|
||||||
|
switch seg {
|
||||||
|
case ".":
|
||||||
|
if len(sel) > 0 && sel[len(sel)-1].Identity() {
|
||||||
|
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
|
||||||
|
}
|
||||||
|
sel = append(sel, Identity)
|
||||||
|
case "[]":
|
||||||
|
sel = append(sel, segment{tok, false, opt, true, nil, "", 0})
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
|
||||||
|
lookup := seg[1 : len(seg)-1]
|
||||||
|
|
||||||
|
if indexRegex.MatchString(lookup) { // index
|
||||||
|
idx, err := strconv.Atoi(lookup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newParseError("invalid index", str, col, tok)
|
||||||
|
}
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, index: idx})
|
||||||
|
} else if strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\"") { // explicit field
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]})
|
||||||
|
} else if sliceRegex.MatchString(lookup) { // slice [3:5] or [:5] or [3:]
|
||||||
|
var rng []int
|
||||||
|
splt := strings.Split(lookup, ":")
|
||||||
|
if splt[0] == "" {
|
||||||
|
rng = append(rng, 0)
|
||||||
|
} else {
|
||||||
|
i, err := strconv.Atoi(splt[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, newParseError("invalid slice index", str, col, tok)
|
||||||
|
}
|
||||||
|
rng = append(rng, i)
|
||||||
|
}
|
||||||
|
if splt[1] != "" {
|
||||||
|
i, err := strconv.Atoi(splt[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, newParseError("invalid slice index", str, col, tok)
|
||||||
|
}
|
||||||
|
rng = append(rng, i)
|
||||||
|
}
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, slice: rng})
|
||||||
|
} else {
|
||||||
|
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
||||||
|
}
|
||||||
|
} else if fieldRegex.MatchString(seg) {
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]})
|
||||||
|
} else {
|
||||||
|
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
col += len(tok)
|
||||||
|
}
|
||||||
|
return sel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustParse(sel string) Selector {
|
||||||
|
s, err := Parse(sel)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenize(str string) []string {
|
||||||
|
var toks []string
|
||||||
|
col := 0
|
||||||
|
ofs := 0
|
||||||
|
ctx := ""
|
||||||
|
|
||||||
|
for col < len(str) {
|
||||||
|
char := string(str[col])
|
||||||
|
|
||||||
|
if char == "\"" && string(str[col-1]) != "\\" {
|
||||||
|
col++
|
||||||
|
if ctx == "\"" {
|
||||||
|
ctx = ""
|
||||||
|
} else {
|
||||||
|
ctx = "\""
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx == "\"" {
|
||||||
|
col++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == "." || char == "[" {
|
||||||
|
if ofs < col {
|
||||||
|
toks = append(toks, str[ofs:col])
|
||||||
|
}
|
||||||
|
ofs = col
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
}
|
||||||
|
|
||||||
|
if ofs < col && ctx != "\"" {
|
||||||
|
toks = append(toks, str[ofs:col])
|
||||||
|
}
|
||||||
|
|
||||||
|
return toks
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseerr struct {
|
||||||
|
msg string
|
||||||
|
src string
|
||||||
|
col int
|
||||||
|
tok string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Name() string {
|
||||||
|
return "ParseError"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Message() string {
|
||||||
|
return p.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Column() int {
|
||||||
|
return p.col
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Error() string {
|
||||||
|
return p.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Source() string {
|
||||||
|
return p.src
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Token() string {
|
||||||
|
return p.tok
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParseError(message string, source string, column int, token string) error {
|
||||||
|
return parseerr{message, source, column, token}
|
||||||
|
}
|
||||||
253
capability/policy/selector/selector.go
Normal file
253
capability/policy/selector/selector.go
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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/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
|
||||||
|
} else if seg.Iterator() {
|
||||||
|
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
||||||
|
var many []ipld.Node
|
||||||
|
it := cur.ListIterator()
|
||||||
|
for {
|
||||||
|
if it.Done() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, many, nil
|
||||||
|
} else if cur != nil && cur.Kind() == datamodel.Kind_Map {
|
||||||
|
var many []ipld.Node
|
||||||
|
it := cur.MapIterator()
|
||||||
|
for {
|
||||||
|
if it.Done() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, many, nil
|
||||||
|
} else if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if seg.Field() != "" {
|
||||||
|
at = append(at, seg.Field())
|
||||||
|
if cur != nil && cur.Kind() == datamodel.Kind_Map {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur = n
|
||||||
|
} else 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 if seg.Slice() != nil {
|
||||||
|
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
||||||
|
return nil, nil, newResolutionError("list slice selection not yet implemented", at)
|
||||||
|
} else if cur != nil && cur.Kind() == datamodel.Kind_Bytes {
|
||||||
|
return nil, nil, newResolutionError("bytes slice selection not yet implemented", at)
|
||||||
|
} else if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("can not index: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
at = append(at, fmt.Sprintf("%d", seg.Index()))
|
||||||
|
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
||||||
|
idx := int64(seg.Index())
|
||||||
|
if idx < 0 {
|
||||||
|
idx = cur.Length() + idx
|
||||||
|
}
|
||||||
|
n, err := cur.LookupByIndex(idx)
|
||||||
|
if err != nil {
|
||||||
|
if isMissing(err) {
|
||||||
|
if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur = n
|
||||||
|
} else 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cur, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
}
|
||||||
499
capability/policy/selector/selector_test.go
Normal file
499
capability/policy/selector/selector_test.go
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
|
"github.com/ipld/go-ipld-prime/printer"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
t.Run("identity", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("field", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".foo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit field", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.["foo"]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Equal(t, sel[1].Field(), "foo")
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("index", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[138]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Equal(t, sel[1].Index(), 138)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative index", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[-138]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Equal(t, sel[1].Index(), -138)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("iterator", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.True(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional field", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".foo?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.True(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional explicit field", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.["foo"]?`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Equal(t, sel[1].Field(), "foo")
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional index", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[138]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Equal(t, sel[1].Index(), 138)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional iterator", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.True(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nesting", func(t *testing.T) {
|
||||||
|
str := `.foo.["bar"].[138]?.baz[1:]`
|
||||||
|
sel, err := Parse(str)
|
||||||
|
require.NoError(t, err)
|
||||||
|
printSegments(sel)
|
||||||
|
require.Equal(t, str, sel.String())
|
||||||
|
require.Equal(t, 7, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.True(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
require.False(t, sel[2].Identity())
|
||||||
|
require.False(t, sel[2].Optional())
|
||||||
|
require.False(t, sel[2].Iterator())
|
||||||
|
require.Empty(t, sel[2].Slice())
|
||||||
|
require.Equal(t, sel[2].Field(), "bar")
|
||||||
|
require.Empty(t, sel[2].Index())
|
||||||
|
require.True(t, sel[3].Identity())
|
||||||
|
require.False(t, sel[3].Optional())
|
||||||
|
require.False(t, sel[3].Iterator())
|
||||||
|
require.Empty(t, sel[3].Slice())
|
||||||
|
require.Empty(t, sel[3].Field())
|
||||||
|
require.Empty(t, sel[3].Index())
|
||||||
|
require.False(t, sel[4].Identity())
|
||||||
|
require.True(t, sel[4].Optional())
|
||||||
|
require.False(t, sel[4].Iterator())
|
||||||
|
require.Empty(t, sel[4].Slice())
|
||||||
|
require.Empty(t, sel[4].Field())
|
||||||
|
require.Equal(t, sel[4].Index(), 138)
|
||||||
|
require.False(t, sel[5].Identity())
|
||||||
|
require.False(t, sel[5].Optional())
|
||||||
|
require.False(t, sel[5].Iterator())
|
||||||
|
require.Empty(t, sel[5].Slice())
|
||||||
|
require.Equal(t, sel[5].Field(), "baz")
|
||||||
|
require.Empty(t, sel[5].Index())
|
||||||
|
require.False(t, sel[6].Identity())
|
||||||
|
require.False(t, sel[6].Optional())
|
||||||
|
require.False(t, sel[6].Iterator())
|
||||||
|
require.Equal(t, sel[6].Slice(), []int{1})
|
||||||
|
require.Empty(t, sel[6].Field())
|
||||||
|
require.Empty(t, sel[6].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non dotted", func(t *testing.T) {
|
||||||
|
_, err := Parse("foo")
|
||||||
|
require.NotNil(t, err)
|
||||||
|
fmt.Println(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non quoted", func(t *testing.T) {
|
||||||
|
_, err := Parse(".[foo]")
|
||||||
|
require.NotNil(t, err)
|
||||||
|
fmt.Println(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSegments(s Selector) {
|
||||||
|
for i, seg := range s {
|
||||||
|
fmt.Printf("%d: %s\n", i, seg.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelect(t *testing.T) {
|
||||||
|
type name struct {
|
||||||
|
First string
|
||||||
|
Middle *string
|
||||||
|
Last string
|
||||||
|
}
|
||||||
|
type interest struct {
|
||||||
|
Name string
|
||||||
|
Outdoor bool
|
||||||
|
Experience int
|
||||||
|
}
|
||||||
|
type user struct {
|
||||||
|
Name name
|
||||||
|
Age int
|
||||||
|
Nationalities []string
|
||||||
|
Interests []interest
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := ipld.LoadSchemaBytes([]byte(`
|
||||||
|
type User struct {
|
||||||
|
name Name
|
||||||
|
age Int
|
||||||
|
nationalities [String]
|
||||||
|
interests [Interest]
|
||||||
|
}
|
||||||
|
type Name struct {
|
||||||
|
first String
|
||||||
|
middle optional String
|
||||||
|
last String
|
||||||
|
}
|
||||||
|
type Interest struct {
|
||||||
|
name String
|
||||||
|
outdoor Bool
|
||||||
|
experience Int
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
typ := ts.TypeByName("User")
|
||||||
|
|
||||||
|
am := "Joan"
|
||||||
|
alice := user{
|
||||||
|
Name: name{First: "Alice", Middle: &am, Last: "Wonderland"},
|
||||||
|
Age: 24,
|
||||||
|
Nationalities: []string{"British"},
|
||||||
|
Interests: []interest{
|
||||||
|
{Name: "Cycling", Outdoor: true, Experience: 4},
|
||||||
|
{Name: "Chess", Outdoor: false, Experience: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bob := user{
|
||||||
|
Name: name{First: "Bob", Last: "Builder"},
|
||||||
|
Age: 35,
|
||||||
|
Nationalities: []string{"Canadian", "South African"},
|
||||||
|
Interests: []interest{
|
||||||
|
{Name: "Snowboarding", Outdoor: true, Experience: 8},
|
||||||
|
{Name: "Reading", Outdoor: false, Experience: 25},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
anode := bindnode.Wrap(&alice, typ)
|
||||||
|
bnode := bindnode.Wrap(&bob, typ)
|
||||||
|
|
||||||
|
t.Run("identity", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
one, many, err := Select(sel, anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, one)
|
||||||
|
require.Empty(t, many)
|
||||||
|
|
||||||
|
fmt.Println(printer.Sprint(one))
|
||||||
|
|
||||||
|
age := must.Int(must.Node(one.LookupByString("age")))
|
||||||
|
require.Equal(t, int64(alice.Age), age)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nested property", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".name.first")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
one, many, err := Select(sel, anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, one)
|
||||||
|
require.Empty(t, many)
|
||||||
|
|
||||||
|
fmt.Println(printer.Sprint(one))
|
||||||
|
|
||||||
|
name := must.String(one)
|
||||||
|
require.Equal(t, alice.Name.First, name)
|
||||||
|
|
||||||
|
one, many, err = Select(sel, bnode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, one)
|
||||||
|
require.Empty(t, many)
|
||||||
|
|
||||||
|
fmt.Println(printer.Sprint(one))
|
||||||
|
|
||||||
|
name = must.String(one)
|
||||||
|
require.Equal(t, bob.Name.First, name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional nested property", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".name.middle?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
one, many, err := Select(sel, anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, one)
|
||||||
|
require.Empty(t, many)
|
||||||
|
|
||||||
|
fmt.Println(printer.Sprint(one))
|
||||||
|
|
||||||
|
name := must.String(one)
|
||||||
|
require.Equal(t, *alice.Name.Middle, name)
|
||||||
|
|
||||||
|
one, many, err = Select(sel, bnode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, one)
|
||||||
|
require.Empty(t, many)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not exists", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".name.foo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
one, many, err := Select(sel, anode)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Empty(t, one)
|
||||||
|
require.Empty(t, many)
|
||||||
|
|
||||||
|
fmt.Println(err)
|
||||||
|
|
||||||
|
require.ErrorAs(t, err, &resolutionerr{}, "error was not a resolution error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional not exists", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".name.foo?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
one, many, err := Select(sel, anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, one)
|
||||||
|
require.Empty(t, many)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("iterator", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".interests[]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
one, many, err := Select(sel, anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, one)
|
||||||
|
require.NotEmpty(t, many)
|
||||||
|
|
||||||
|
for _, n := range many {
|
||||||
|
fmt.Println(printer.Sprint(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
iname := must.String(must.Node(many[0].LookupByString("name")))
|
||||||
|
require.Equal(t, alice.Interests[0].Name, iname)
|
||||||
|
|
||||||
|
iname = must.String(must.Node(many[1].LookupByString("name")))
|
||||||
|
require.Equal(t, alice.Interests[1].Name, iname)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("map iterator", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".interests[0][]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
one, many, err := Select(sel, anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, one)
|
||||||
|
require.NotEmpty(t, many)
|
||||||
|
|
||||||
|
for _, n := range many {
|
||||||
|
fmt.Println(printer.Sprint(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, alice.Interests[0].Name, must.String(many[0]))
|
||||||
|
require.Equal(t, alice.Interests[0].Experience, int(must.Int(many[2])))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzParse(f *testing.F) {
|
||||||
|
selectorCorpus := []string{
|
||||||
|
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
||||||
|
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
|
||||||
|
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
|
||||||
|
`.length`, `.[4]`,
|
||||||
|
}
|
||||||
|
for _, selector := range selectorCorpus {
|
||||||
|
f.Add(selector)
|
||||||
|
}
|
||||||
|
f.Fuzz(func(t *testing.T, selector string) {
|
||||||
|
// only look for panic()
|
||||||
|
_, _ = Parse(selector)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzParseAndSelect(f *testing.F) {
|
||||||
|
selectorCorpus := []string{
|
||||||
|
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
||||||
|
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
|
||||||
|
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
|
||||||
|
`.length`, `.[4]`,
|
||||||
|
}
|
||||||
|
subjectCorpus := []string{
|
||||||
|
`{"x":1}`, `[1, 2]`, `null`, `[[1], 2, [3]]`, `{"x": 1 }`, `{"x": 1}`,
|
||||||
|
`[1, 2]`, `[1, 2]`, `"Hi"`, `{"/":{"bytes":"AAE"}`, `[0, 1, 2]`,
|
||||||
|
`[0, 1, 2]`, `[0, 1, 2]`, `"hello"`, `{"/":{"bytes":"AAEC"}}`, `{}`,
|
||||||
|
`null`, `[]`, `{}`, `[1, 2]`, `[0, 1]`, `null`, `[[1], 2, [3]]`, `{}`,
|
||||||
|
`null`, `[]`, `[1, 2]`, `[0, 1]`,
|
||||||
|
}
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
switch {
|
||||||
|
case i < len(selectorCorpus) && i < len(subjectCorpus):
|
||||||
|
f.Add(selectorCorpus[i], subjectCorpus[i])
|
||||||
|
continue
|
||||||
|
case i > len(selectorCorpus):
|
||||||
|
f.Add("", subjectCorpus[i])
|
||||||
|
continue
|
||||||
|
case i > len(subjectCorpus):
|
||||||
|
f.Add(selectorCorpus[i], "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, selector, subject string) {
|
||||||
|
sel, err := Parse(selector)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
np := basicnode.Prototype.Any
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
err = dagjson.Decode(nb, strings.NewReader(subject))
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
node := nb.Build()
|
||||||
|
if node == nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for panic()
|
||||||
|
_, _, _ = Select(sel, node)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
package selector_test
|
package selector_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/selector"
|
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSupported Forms runs tests against the Selector according to the
|
// TestSupported Forms runs tests against the Selector according to the
|
||||||
@@ -23,16 +26,18 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
Output string
|
Output string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass and return a node
|
// Pass
|
||||||
for _, testcase := range []Testcase{
|
for _, testcase := range []Testcase{
|
||||||
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
|
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
|
||||||
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
|
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
|
||||||
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `[]`},
|
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `()`},
|
||||||
|
{Name: "Optional Iterator", Selector: `.[][]?`, Input: `[[1], 2, [3]]`, Output: `[1, 3]`},
|
||||||
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
|
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
|
||||||
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
|
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
|
||||||
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
|
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
|
||||||
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
|
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
|
||||||
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}}`, Output: `0`},
|
{Name: "String Index", Selector: `.[0]`, Input: `"Hi"`, Output: `"H"`},
|
||||||
|
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}`, Output: `0`},
|
||||||
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
||||||
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
|
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
|
||||||
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
||||||
@@ -47,16 +52,35 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
res, err := sel.Select(makeNode(t, tc.Input))
|
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, res)
|
require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set)
|
||||||
|
|
||||||
|
// make an IPLD List node from a []datamodel.Node
|
||||||
|
if node == nil {
|
||||||
|
nb := basicnode.Prototype.List.NewBuilder()
|
||||||
|
la, err := nb.BeginList(int64(len(nodes)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, n := range nodes {
|
||||||
|
// TODO: This code is probably not needed if the Select operation properly prunes nil values - e.g.: Optional Iterator
|
||||||
|
if n == nil {
|
||||||
|
n = datamodel.Null
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, la.AssembleValue().AssignNode(n))
|
||||||
|
}
|
||||||
|
require.NoError(t, la.Finish())
|
||||||
|
|
||||||
|
node = nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
exp := makeNode(t, tc.Output)
|
exp := makeNode(t, tc.Output)
|
||||||
require.True(t, ipld.DeepEqual(exp, res))
|
equalIPLD(t, exp, node)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// No error and return null, as optional
|
// null
|
||||||
for _, testcase := range []Testcase{
|
for _, testcase := range []Testcase{
|
||||||
{Name: "Optional Missing Key", Selector: `.x?`, Input: `{}`},
|
{Name: "Optional Missing Key", Selector: `.x?`, Input: `{}`},
|
||||||
{Name: "Optional Null Key", Selector: `.x?`, Input: `null`},
|
{Name: "Optional Null Key", Selector: `.x?`, Input: `null`},
|
||||||
@@ -73,15 +97,19 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
res, err := sel.Select(makeNode(t, tc.Input))
|
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, res)
|
// TODO: should Select return a single node which is sometimes a list or null?
|
||||||
|
// require.Equal(t, datamodel.Null, node)
|
||||||
|
assert.Nil(t, node)
|
||||||
|
assert.Empty(t, nodes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail to select and return an error
|
// error
|
||||||
for _, testcase := range []Testcase{
|
for _, testcase := range []Testcase{
|
||||||
{Name: "Null Iterator", Selector: `.[]`, Input: `null`},
|
{Name: "Null Iterator", Selector: `.[]`, Input: `null`},
|
||||||
|
{Name: "Nested Iterator", Selector: `.[][]`, Input: `[[1], 2, [3]]`},
|
||||||
{Name: "Missing Key", Selector: `.x`, Input: `{}`},
|
{Name: "Missing Key", Selector: `.x`, Input: `{}`},
|
||||||
{Name: "Null Key", Selector: `.x`, Input: `null`},
|
{Name: "Null Key", Selector: `.x`, Input: `null`},
|
||||||
{Name: "Array Key", Selector: `.x`, Input: `[]`},
|
{Name: "Array Key", Selector: `.x`, Input: `[]`},
|
||||||
@@ -96,13 +124,31 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
res, err := sel.Select(makeNode(t, tc.Input))
|
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, res)
|
assert.Nil(t, node)
|
||||||
|
assert.Empty(t, nodes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func equalIPLD(t *testing.T, expected datamodel.Node, actual datamodel.Node) bool {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
exp, act := &bytes.Buffer{}, &bytes.Buffer{}
|
||||||
|
if err := dagjson.Encode(expected, exp); err != nil {
|
||||||
|
return assert.Fail(t, "Failed to encode json for expected IPLD node")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dagjson.Encode(actual, act); err != nil {
|
||||||
|
return assert.Fail(t, "Failed to encode JSON for actual IPLD node")
|
||||||
|
}
|
||||||
|
|
||||||
|
require.JSONEq(t, exp.String(), act.String())
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func makeNode(t *testing.T, dagJsonInput string) ipld.Node {
|
func makeNode(t *testing.T, dagJsonInput string) ipld.Node {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ type Payload struct {
|
|||||||
nonce Bytes
|
nonce Bytes
|
||||||
|
|
||||||
# Arbitrary Metadata
|
# Arbitrary Metadata
|
||||||
meta optional {String : Any}
|
meta {String : Any}
|
||||||
|
|
||||||
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
||||||
nbf optional Int
|
nbf optional Int
|
||||||
33
delegation/encoding.go
Normal file
33
delegation/encoding.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package delegation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *PayloadModel) EncodeDagCbor() ([]byte, error) {
|
||||||
|
return ipld.Marshal(dagcbor.Encode, p, PayloadType())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PayloadModel) EncodeDagJson() ([]byte, error) {
|
||||||
|
return ipld.Marshal(dagjson.Encode, p, PayloadType())
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeDagCbor(data []byte) (*PayloadModel, error) {
|
||||||
|
var p PayloadModel
|
||||||
|
_, err := ipld.Unmarshal(data, dagcbor.Decode, &p, PayloadType())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeDagJson(data []byte) (*PayloadModel, error) {
|
||||||
|
var p PayloadModel
|
||||||
|
_, err := ipld.Unmarshal(data, dagjson.Decode, &p, PayloadType())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
69
delegation/schema.go
Normal file
69
delegation/schema.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package delegation
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed delegation.ipldsch
|
||||||
|
var schemaBytes []byte
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
ts *schema.TypeSystem
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustLoadSchema() *schema.TypeSystem {
|
||||||
|
once.Do(func() {
|
||||||
|
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||||
|
}
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
func PayloadType() schema.Type {
|
||||||
|
return mustLoadSchema().TypeByName("Payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayloadModel struct {
|
||||||
|
// Issuer DID (sender)
|
||||||
|
Iss string
|
||||||
|
// Audience DID (receiver)
|
||||||
|
Aud string
|
||||||
|
// Principal that the chain is about (the Subject)
|
||||||
|
// optional: can be nil
|
||||||
|
Sub *string
|
||||||
|
|
||||||
|
// The Command to eventually invoke
|
||||||
|
Cmd string
|
||||||
|
|
||||||
|
// The delegation policy
|
||||||
|
Pol datamodel.Node
|
||||||
|
|
||||||
|
// A unique, random nonce
|
||||||
|
Nonce []byte
|
||||||
|
|
||||||
|
// Arbitrary Metadata
|
||||||
|
// optional: can be nil
|
||||||
|
Meta MetaModel
|
||||||
|
|
||||||
|
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
||||||
|
// optional: can be nil
|
||||||
|
Nbf *int64
|
||||||
|
// The timestamp at which the Invocation becomes invalid
|
||||||
|
// optional: can be nil
|
||||||
|
Exp *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetaModel struct {
|
||||||
|
Keys []string
|
||||||
|
Values map[string]datamodel.Node
|
||||||
|
}
|
||||||
71
delegation/schema_test.go
Normal file
71
delegation/schema_test.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package delegation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSchemaRoundTrip(t *testing.T) {
|
||||||
|
const delegationJson = `
|
||||||
|
{
|
||||||
|
"aud":"did:key:def456",
|
||||||
|
"cmd":"/foo/bar",
|
||||||
|
"exp":123456,
|
||||||
|
"iss":"did:key:abc123",
|
||||||
|
"meta":{
|
||||||
|
"bar":"baaar",
|
||||||
|
"foo":"fooo"
|
||||||
|
},
|
||||||
|
"nbf":123456,
|
||||||
|
"nonce":{
|
||||||
|
"/":{
|
||||||
|
"bytes":"c3VwZXItcmFuZG9t"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pol":[
|
||||||
|
["==", ".status", "draft"],
|
||||||
|
["all", ".reviewer", [
|
||||||
|
["like", ".email", "*@example.com"]]
|
||||||
|
],
|
||||||
|
["any", ".tags", [
|
||||||
|
["or", [
|
||||||
|
["==", ".", "news"],
|
||||||
|
["==", ".", "press"]]
|
||||||
|
]]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"sub":""
|
||||||
|
}
|
||||||
|
`
|
||||||
|
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
||||||
|
// function: DecodeDagJson() EncodeDagCbor() DecodeDagCbor() EncodeDagJson()
|
||||||
|
|
||||||
|
p1, err := DecodeDagJson([]byte(delegationJson))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cborBytes, err := p1.EncodeDagCbor()
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Println("cborBytes length", len(cborBytes))
|
||||||
|
fmt.Println("cbor", string(cborBytes))
|
||||||
|
|
||||||
|
p2, err := DecodeDagCbor(cborBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Println("read Cbor", p2)
|
||||||
|
|
||||||
|
readJson, err := p2.EncodeDagJson()
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Println("readJson length", len(readJson))
|
||||||
|
fmt.Println("json: ", string(readJson))
|
||||||
|
|
||||||
|
require.JSONEq(t, delegationJson, string(readJson))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSchemaLoad(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
delegation/view.go
Normal file
87
delegation/view.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package delegation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
type View struct {
|
||||||
|
// Issuer DID (sender)
|
||||||
|
Issuer did.DID
|
||||||
|
// Audience DID (receiver)
|
||||||
|
Audience did.DID
|
||||||
|
// Principal that the chain is about (the Subject)
|
||||||
|
Subject did.DID
|
||||||
|
// The Command to eventually invoke
|
||||||
|
Command *command.Command
|
||||||
|
// The delegation policy
|
||||||
|
Policy policy.Policy
|
||||||
|
// A unique, random nonce
|
||||||
|
Nonce []byte
|
||||||
|
// Arbitrary Metadata
|
||||||
|
Meta map[string]datamodel.Node
|
||||||
|
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
||||||
|
NotBefore time.Time
|
||||||
|
// The timestamp at which the Invocation becomes invalid
|
||||||
|
Expiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewFromModel build a decoded view of the raw IPLD data.
|
||||||
|
// This function also serves as validation.
|
||||||
|
func ViewFromModel(m PayloadModel) (*View, error) {
|
||||||
|
var view View
|
||||||
|
var err error
|
||||||
|
|
||||||
|
view.Issuer, err = did.Parse(m.Iss)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse iss: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Audience, err = did.Parse(m.Aud)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse audience: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Sub != nil {
|
||||||
|
view.Subject, err = did.Parse(*m.Sub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse subject: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
view.Subject = did.Undef
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Command, err = command.Parse(m.Cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Policy, err = policy.FromIPLD(m.Pol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse policy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.Nonce) == 0 {
|
||||||
|
return nil, fmt.Errorf("nonce is required")
|
||||||
|
}
|
||||||
|
view.Nonce = m.Nonce
|
||||||
|
|
||||||
|
// TODO: copy?
|
||||||
|
view.Meta = m.Meta.Values
|
||||||
|
|
||||||
|
if m.Nbf != nil {
|
||||||
|
view.NotBefore = time.Unix(*m.Nbf, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Exp != nil {
|
||||||
|
view.Expiration = time.Unix(*m.Exp, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &view, nil
|
||||||
|
}
|
||||||
86
did/did.go
Normal file
86
did/did.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package did
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
mbase "github.com/multiformats/go-multibase"
|
||||||
|
varint "github.com/multiformats/go-varint"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Prefix = "did:"
|
||||||
|
const KeyPrefix = "did:key:"
|
||||||
|
|
||||||
|
const DIDCore = 0x0d1d
|
||||||
|
const Ed25519 = 0xed
|
||||||
|
|
||||||
|
var MethodOffset = varint.UvarintSize(uint64(DIDCore))
|
||||||
|
|
||||||
|
type DID struct {
|
||||||
|
key bool
|
||||||
|
str string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undef can be used to represent a nil or undefined DID, using DID{}
|
||||||
|
// directly is also acceptable.
|
||||||
|
var Undef = DID{}
|
||||||
|
|
||||||
|
func (d DID) Defined() bool {
|
||||||
|
return d.str != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DID) Bytes() []byte {
|
||||||
|
if !d.Defined() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []byte(d.str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DID) DID() DID {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// String formats the decentralized identity document (DID) as a string.
|
||||||
|
func (d DID) String() string {
|
||||||
|
if d.key {
|
||||||
|
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.str))
|
||||||
|
return "did:key:" + key
|
||||||
|
}
|
||||||
|
return "did:" + d.str[MethodOffset:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decode(bytes []byte) (DID, error) {
|
||||||
|
code, _, err := varint.FromUvarint(bytes)
|
||||||
|
if err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
if code == Ed25519 {
|
||||||
|
return DID{str: string(bytes), key: true}, nil
|
||||||
|
} else if code == DIDCore {
|
||||||
|
return DID{str: string(bytes)}, nil
|
||||||
|
}
|
||||||
|
return Undef, fmt.Errorf("unsupported DID encoding: 0x%x", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(str string) (DID, error) {
|
||||||
|
if !strings.HasPrefix(str, Prefix) {
|
||||||
|
return Undef, fmt.Errorf("must start with 'did:'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(str, KeyPrefix) {
|
||||||
|
code, bytes, err := mbase.Decode(str[len(KeyPrefix):])
|
||||||
|
if err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
if code != mbase.Base58BTC {
|
||||||
|
return Undef, fmt.Errorf("not Base58BTC encoded")
|
||||||
|
}
|
||||||
|
return Decode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, MethodOffset)
|
||||||
|
varint.PutUvarint(buf, DIDCore)
|
||||||
|
suffix, _ := strings.CutPrefix(str, Prefix)
|
||||||
|
buf = append(buf, suffix...)
|
||||||
|
return DID{str: string(buf)}, nil
|
||||||
|
}
|
||||||
79
did/did_test.go
Normal file
79
did/did_test.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package did
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseDIDKey(t *testing.T) {
|
||||||
|
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
|
d, err := Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if d.String() != str {
|
||||||
|
t.Fatalf("expected %v to equal %v", d.String(), str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeDIDKey(t *testing.T) {
|
||||||
|
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
|
d0, err := Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
d1, err := Decode(d0.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if d1.String() != str {
|
||||||
|
t.Fatalf("expected %v to equal %v", d1.String(), str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDIDWeb(t *testing.T) {
|
||||||
|
str := "did:web:up.web3.storage"
|
||||||
|
d, err := Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if d.String() != str {
|
||||||
|
t.Fatalf("expected %v to equal %v", d.String(), str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeDIDWeb(t *testing.T) {
|
||||||
|
str := "did:web:up.web3.storage"
|
||||||
|
d0, err := Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
d1, err := Decode(d0.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if d1.String() != str {
|
||||||
|
t.Fatalf("expected %v to equal %v", d1.String(), str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEquivalence(t *testing.T) {
|
||||||
|
u0 := DID{}
|
||||||
|
u1 := Undef
|
||||||
|
if u0 != u1 {
|
||||||
|
t.Fatalf("undef DID not equivalent")
|
||||||
|
}
|
||||||
|
|
||||||
|
d0, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d1, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d0 != d1 {
|
||||||
|
t.Fatalf("two equivalent DID not equivalent")
|
||||||
|
}
|
||||||
|
}
|
||||||
37
go.mod
37
go.mod
@@ -1,36 +1,31 @@
|
|||||||
module code.sonr.org/go/ucan
|
module github.com/ucan-wg/go-ucan/v1
|
||||||
|
|
||||||
go 1.24.4
|
go 1.21
|
||||||
|
|
||||||
toolchain go1.24.5
|
toolchain go1.22.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.sonr.org/go/did-it v1.0.0
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/avast/retry-go/v4 v4.6.1
|
github.com/ipfs/go-cid v0.4.1
|
||||||
github.com/ipfs/go-cid v0.5.0
|
|
||||||
github.com/ipld/go-ipld-prime v0.21.0
|
github.com/ipld/go-ipld-prime v0.21.0
|
||||||
github.com/multiformats/go-multibase v0.2.0
|
github.com/multiformats/go-multibase v0.0.3
|
||||||
github.com/multiformats/go-multicodec v0.9.0
|
github.com/multiformats/go-varint v0.0.6
|
||||||
github.com/multiformats/go-multihash v0.2.3
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/multiformats/go-varint v0.1.0
|
|
||||||
github.com/stretchr/testify v1.10.0
|
|
||||||
github.com/ucan-wg/go-varsig v1.0.0
|
|
||||||
golang.org/x/crypto v0.45.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
github.com/multiformats/go-base32 v0.0.3 // indirect
|
||||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
github.com/multiformats/go-base36 v0.1.0 // indirect
|
||||||
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polydawn/refmt v0.89.0 // indirect
|
github.com/polydawn/refmt v0.89.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/crypto v0.1.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.1.6 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
61
go.sum
61
go.sum
@@ -1,50 +1,46 @@
|
|||||||
code.sonr.org/go/did-it v1.0.0 h1:Wh8igUkD6cuf0Ul3gawi27z2/M1YfdnQ/mD9gBq/2EU=
|
|
||||||
code.sonr.org/go/did-it v1.0.0/go.mod h1:PFK6ItvNyB2xbnVqipBbkN9BK1Sq+E2lf1YfOyCA0Og=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
|
|
||||||
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
|
||||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
|
||||||
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
|
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
|
||||||
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||||
|
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI=
|
||||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
|
||||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4=
|
||||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
|
||||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk=
|
||||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
|
||||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||||
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
||||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||||
github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI=
|
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
|
||||||
github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI=
|
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
|
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
|
||||||
@@ -59,29 +55,26 @@ github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hg
|
|||||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/ucan-wg/go-varsig v1.0.0 h1:Hrc437Zg+B5Eoajg+qZQZI3Q3ocPyjlnp3/Bz9ZnlWw=
|
|
||||||
github.com/ucan-wg/go-varsig v1.0.0/go.mod h1:Sakln6IPooDPH+ClQ0VvR09TuwUhHcfLqcPiPkMZGh0=
|
|
||||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
|
||||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
||||||
|
|||||||
37
invocation/invocation.ipldsch
Normal file
37
invocation/invocation.ipldsch
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# The Invocation Payload attaches sender, receiver, and provenance to the Task.
|
||||||
|
# type Payload struct {
|
||||||
|
# # The DID of the Invoker
|
||||||
|
# iss DID
|
||||||
|
# # The Subject being invoked
|
||||||
|
# sub DID
|
||||||
|
# # The DID of the intended Executor if different from the Subject
|
||||||
|
# aud optional DID
|
||||||
|
#
|
||||||
|
# # The Command
|
||||||
|
# cmd Command
|
||||||
|
# # The Command's Arguments
|
||||||
|
# args {String : Any}
|
||||||
|
#
|
||||||
|
# # A unique, random nonce
|
||||||
|
# nonce optional String
|
||||||
|
#
|
||||||
|
# # Arbitrary Metadata
|
||||||
|
# meta optional {String : Any}
|
||||||
|
# # Delegations that prove the chain of authority
|
||||||
|
# prf [&Delegation]
|
||||||
|
# # The timestamp at which the Invocation becomes invalid
|
||||||
|
# exp optional Integer
|
||||||
|
# # An OPTIONAL timestamp at which the Invocation was created
|
||||||
|
# iat optional Integer
|
||||||
|
#
|
||||||
|
# # An OPTIONAL CID of the Receipt that enqueued the Task
|
||||||
|
# cause optional &Receipt
|
||||||
|
# }
|
||||||
|
|
||||||
|
type DID string
|
||||||
|
|
||||||
|
type Foo struct {
|
||||||
|
iss DID
|
||||||
|
exp optional Int
|
||||||
|
prf [String]
|
||||||
|
}
|
||||||
187
pkg/args/args.go
187
pkg/args/args.go
@@ -1,187 +0,0 @@
|
|||||||
// Package args provides the type that represents the Arguments passed to
|
|
||||||
// a command within an invocation.Token as well as a convenient Add method
|
|
||||||
// to incrementally build the underlying map.
|
|
||||||
package args
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"iter"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/printer"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/limits"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/literal"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrNotFound = errors.New("key not found in meta")
|
|
||||||
|
|
||||||
// Args are the Command's arguments when an invocation Token is processed by the executor.
|
|
||||||
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
|
||||||
// and transformations, while hiding the IPLD complexity from the caller.
|
|
||||||
type Args struct {
|
|
||||||
// This type must be compatible with the IPLD type represented by the IPLD
|
|
||||||
// schema { String : Any }.
|
|
||||||
|
|
||||||
Keys []string
|
|
||||||
Values map[string]ipld.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a pointer to an initialized Args value.
|
|
||||||
func New() *Args {
|
|
||||||
return &Args{
|
|
||||||
Values: map[string]ipld.Node{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode retrieves a value as a raw IPLD node.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
func (a *Args) GetNode(key string) (ipld.Node, error) {
|
|
||||||
v, ok := a.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add inserts a key/value pair in the Args set.
|
|
||||||
//
|
|
||||||
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
|
||||||
func (a *Args) Add(key string, val any) error {
|
|
||||||
if _, ok := a.Values[key]; ok {
|
|
||||||
return fmt.Errorf("duplicate key %q", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
node, err := literal.Any(val)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
|
|
||||||
return fmt.Errorf("value for key %q: %w", key, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.Values[key] = node
|
|
||||||
a.Keys = append(a.Keys, key)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Iterator interface {
|
|
||||||
Iter() iter.Seq2[string, ipld.Node]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include merges the provided arguments into the existing arguments.
|
|
||||||
//
|
|
||||||
// If duplicate keys are encountered, the new value is silently dropped
|
|
||||||
// without causing an error.
|
|
||||||
func (a *Args) Include(other Iterator) {
|
|
||||||
for key, value := range other.Iter() {
|
|
||||||
if _, ok := a.Values[key]; ok {
|
|
||||||
// don't overwrite
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
a.Values[key] = value
|
|
||||||
a.Keys = append(a.Keys, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len return the number of arguments.
|
|
||||||
func (a *Args) Len() int {
|
|
||||||
return len(a.Keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iter iterates over the args key/values
|
|
||||||
func (a *Args) Iter() iter.Seq2[string, ipld.Node] {
|
|
||||||
return func(yield func(string, ipld.Node) bool) {
|
|
||||||
for _, key := range a.Keys {
|
|
||||||
if !yield(key, a.Values[key]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToIPLD wraps an instance of an Args with an ipld.Node.
|
|
||||||
func (a *Args) ToIPLD() (ipld.Node, error) {
|
|
||||||
sort.Strings(a.Keys)
|
|
||||||
|
|
||||||
return qp.BuildMap(basicnode.Prototype.Any, int64(len(a.Keys)), func(ma datamodel.MapAssembler) {
|
|
||||||
for _, key := range a.Keys {
|
|
||||||
qp.MapEntry(ma, key, qp.Node(a.Values[key]))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals tells if two Args hold the same values.
|
|
||||||
func (a *Args) Equals(other *Args) bool {
|
|
||||||
if len(a.Keys) != len(other.Keys) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(a.Values) != len(other.Values) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, key := range a.Keys {
|
|
||||||
if !ipld.DeepEqual(a.Values[key], other.Values[key]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Args) String() string {
|
|
||||||
sort.Strings(a.Keys)
|
|
||||||
|
|
||||||
buf := strings.Builder{}
|
|
||||||
buf.WriteString("{")
|
|
||||||
|
|
||||||
for _, key := range a.Keys {
|
|
||||||
buf.WriteString("\n\t")
|
|
||||||
buf.WriteString(key)
|
|
||||||
buf.WriteString(": ")
|
|
||||||
buf.WriteString(strings.ReplaceAll(printer.Sprint(a.Values[key]), "\n", "\n\t"))
|
|
||||||
buf.WriteString(",")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(a.Keys) > 0 {
|
|
||||||
buf.WriteString("\n")
|
|
||||||
}
|
|
||||||
buf.WriteString("}")
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadOnly returns a read-only version of Args.
|
|
||||||
func (a *Args) ReadOnly() ReadOnly {
|
|
||||||
return ReadOnly{args: a}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone makes a deep copy.
|
|
||||||
func (a *Args) Clone() *Args {
|
|
||||||
res := &Args{
|
|
||||||
Keys: make([]string, len(a.Keys)),
|
|
||||||
Values: make(map[string]ipld.Node, len(a.Values)),
|
|
||||||
}
|
|
||||||
copy(res.Keys, a.Keys)
|
|
||||||
for k, v := range a.Values {
|
|
||||||
res.Values[k] = v
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks that all values in the Args are valid according to UCAN specs
|
|
||||||
func (a *Args) Validate() error {
|
|
||||||
for key, value := range a.Values {
|
|
||||||
if err := limits.ValidateIntegerBoundsIPLD(value); err != nil {
|
|
||||||
return fmt.Errorf("value for key %q: %w", key, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
package args_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"maps"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/args"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/limits"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/literal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestArgs(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
intKey = "intKey"
|
|
||||||
mapKey = "mapKey"
|
|
||||||
nilKey = "nilKey"
|
|
||||||
boolKey = "boolKey"
|
|
||||||
linkKey = "linkKey"
|
|
||||||
listKey = "listKey"
|
|
||||||
nodeKey = "nodeKey"
|
|
||||||
uintKey = "uintKey"
|
|
||||||
bytesKey = "bytesKey"
|
|
||||||
floatKey = "floatKey"
|
|
||||||
stringKey = "stringKey"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
expIntVal = int64(-42)
|
|
||||||
expBoolVal = true
|
|
||||||
expUintVal = uint(42)
|
|
||||||
expStringVal = "stringVal"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
expMapVal = map[string]string{"keyOne": "valOne", "keyTwo": "valTwo"}
|
|
||||||
// expNilVal = (map[string]string)(nil)
|
|
||||||
expLinkVal = cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")
|
|
||||||
expListVal = []string{"elem1", "elem2", "elem3"}
|
|
||||||
expNodeVal = literal.String("nodeVal")
|
|
||||||
expBytesVal = []byte{0xde, 0xad, 0xbe, 0xef}
|
|
||||||
expFloatVal = 42.0
|
|
||||||
)
|
|
||||||
|
|
||||||
argsIn := args.New()
|
|
||||||
|
|
||||||
for _, a := range []struct {
|
|
||||||
key string
|
|
||||||
val any
|
|
||||||
}{
|
|
||||||
{key: intKey, val: expIntVal},
|
|
||||||
{key: mapKey, val: expMapVal},
|
|
||||||
// {key: nilKey, val: expNilVal},
|
|
||||||
{key: boolKey, val: expBoolVal},
|
|
||||||
{key: linkKey, val: expLinkVal},
|
|
||||||
{key: listKey, val: expListVal},
|
|
||||||
{key: uintKey, val: expUintVal},
|
|
||||||
{key: nodeKey, val: expNodeVal},
|
|
||||||
{key: bytesKey, val: expBytesVal},
|
|
||||||
{key: floatKey, val: expFloatVal},
|
|
||||||
{key: stringKey, val: expStringVal},
|
|
||||||
} {
|
|
||||||
require.NoError(t, argsIn.Add(a.key, a.val))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Round-trip to DAG-CBOR
|
|
||||||
argsOut := roundTripThroughDAGCBOR(t, argsIn)
|
|
||||||
assert.ElementsMatch(t, argsIn.Keys, argsOut.Keys)
|
|
||||||
assert.Equal(t, argsIn.Values, argsOut.Values)
|
|
||||||
|
|
||||||
actMapVal := map[string]string{}
|
|
||||||
mit := argsOut.Values[mapKey].MapIterator()
|
|
||||||
|
|
||||||
for !mit.Done() {
|
|
||||||
k, v, err := mit.Next()
|
|
||||||
require.NoError(t, err)
|
|
||||||
ks := must(k.AsString())
|
|
||||||
vs := must(v.AsString())
|
|
||||||
actMapVal[ks] = vs
|
|
||||||
}
|
|
||||||
|
|
||||||
actListVal := []string{}
|
|
||||||
lit := argsOut.Values[listKey].ListIterator()
|
|
||||||
|
|
||||||
for !lit.Done() {
|
|
||||||
_, v, err := lit.Next()
|
|
||||||
require.NoError(t, err)
|
|
||||||
vs := must(v.AsString())
|
|
||||||
|
|
||||||
actListVal = append(actListVal, vs)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, expIntVal, must(argsOut.Values[intKey].AsInt()))
|
|
||||||
assert.Equal(t, expMapVal, actMapVal) // TODO: special accessor
|
|
||||||
// TODO: the nil map comes back empty (but the right type)
|
|
||||||
// assert.Equal(t, expNilVal, actNilVal)
|
|
||||||
assert.Equal(t, expBoolVal, must(argsOut.Values[boolKey].AsBool()))
|
|
||||||
assert.Equal(t, expLinkVal.String(), must(argsOut.Values[linkKey].AsLink()).(datamodel.Link).String()) // TODO: special accessor
|
|
||||||
assert.Equal(t, expListVal, actListVal) // TODO: special accessor
|
|
||||||
assert.Equal(t, expNodeVal, argsOut.Values[nodeKey])
|
|
||||||
assert.Equal(t, expUintVal, uint(must(argsOut.Values[uintKey].AsInt())))
|
|
||||||
assert.Equal(t, expBytesVal, must(argsOut.Values[bytesKey].AsBytes()))
|
|
||||||
assert.Equal(t, expFloatVal, must(argsOut.Values[floatKey].AsFloat()))
|
|
||||||
assert.Equal(t, expStringVal, must(argsOut.Values[stringKey].AsString()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestArgs_Include(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
argsIn := args.New()
|
|
||||||
require.NoError(t, argsIn.Add("key1", "val1"))
|
|
||||||
require.NoError(t, argsIn.Add("key2", "val2"))
|
|
||||||
|
|
||||||
argsOther := args.New()
|
|
||||||
require.NoError(t, argsOther.Add("key2", "valOther")) // This should not overwrite key2 above
|
|
||||||
require.NoError(t, argsOther.Add("key3", "val3"))
|
|
||||||
require.NoError(t, argsOther.Add("key4", "val4"))
|
|
||||||
|
|
||||||
argsIn.Include(argsOther)
|
|
||||||
|
|
||||||
assert.Len(t, argsIn.Values, 4)
|
|
||||||
assert.Equal(t, "val1", must(argsIn.Values["key1"].AsString()))
|
|
||||||
assert.Equal(t, "val2", must(argsIn.Values["key2"].AsString()))
|
|
||||||
assert.Equal(t, "val3", must(argsIn.Values["key3"].AsString()))
|
|
||||||
assert.Equal(t, "val4", must(argsIn.Values["key4"].AsString()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIterCloneEquals(t *testing.T) {
|
|
||||||
a := args.New()
|
|
||||||
|
|
||||||
require.NoError(t, a.Add("foo", "bar"))
|
|
||||||
require.NoError(t, a.Add("baz", 1234))
|
|
||||||
|
|
||||||
expected := map[string]ipld.Node{
|
|
||||||
"foo": basicnode.NewString("bar"),
|
|
||||||
"baz": basicnode.NewInt(1234),
|
|
||||||
}
|
|
||||||
|
|
||||||
// args -> iter
|
|
||||||
require.Equal(t, expected, maps.Collect(a.Iter()))
|
|
||||||
|
|
||||||
// readonly -> iter
|
|
||||||
ro := a.ReadOnly()
|
|
||||||
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
|
||||||
|
|
||||||
// args -> clone -> iter
|
|
||||||
clone := a.Clone()
|
|
||||||
require.Equal(t, expected, maps.Collect(clone.Iter()))
|
|
||||||
|
|
||||||
// readonly -> WriteableClone -> iter
|
|
||||||
wclone := ro.WriteableClone()
|
|
||||||
require.Equal(t, expected, maps.Collect(wclone.Iter()))
|
|
||||||
|
|
||||||
require.True(t, a.Equals(wclone))
|
|
||||||
require.True(t, ro.Equals(wclone.ReadOnly()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInclude(t *testing.T) {
|
|
||||||
a1 := args.New()
|
|
||||||
|
|
||||||
require.NoError(t, a1.Add("samekey", "bar"))
|
|
||||||
require.NoError(t, a1.Add("baz", 1234))
|
|
||||||
|
|
||||||
a2 := args.New()
|
|
||||||
|
|
||||||
require.NoError(t, a2.Add("samekey", "othervalue")) // check no overwrite
|
|
||||||
require.NoError(t, a2.Add("otherkey", 1234))
|
|
||||||
|
|
||||||
a1.Include(a2)
|
|
||||||
|
|
||||||
require.Equal(t, map[string]ipld.Node{
|
|
||||||
"samekey": basicnode.NewString("bar"),
|
|
||||||
"baz": basicnode.NewInt(1234),
|
|
||||||
"otherkey": basicnode.NewInt(1234),
|
|
||||||
}, maps.Collect(a1.Iter()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestArgsIntegerBounds(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
key string
|
|
||||||
val int64
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid int",
|
|
||||||
key: "valid",
|
|
||||||
val: 42,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "max safe integer",
|
|
||||||
key: "max",
|
|
||||||
val: limits.MaxInt53,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "min safe integer",
|
|
||||||
key: "min",
|
|
||||||
val: limits.MinInt53,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "exceeds max safe integer",
|
|
||||||
key: "tooBig",
|
|
||||||
val: limits.MaxInt53 + 1,
|
|
||||||
wantErr: "exceeds safe integer bounds",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "below min safe integer",
|
|
||||||
key: "tooSmall",
|
|
||||||
val: limits.MinInt53 - 1,
|
|
||||||
wantErr: "exceeds safe integer bounds",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate key",
|
|
||||||
key: "duplicate",
|
|
||||||
val: 42,
|
|
||||||
wantErr: "duplicate key",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
a := args.New()
|
|
||||||
require.NoError(t, a.Add("duplicate", 1)) // tests duplicate key
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
err := a.Add(tt.key, tt.val)
|
|
||||||
if tt.wantErr != "" {
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), tt.wantErr)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
val, err := a.GetNode(tt.key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
i, err := val.AsInt()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tt.val, i)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
argsSchema = "type Args { String : Any }"
|
|
||||||
argsName = "Args"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
once sync.Once
|
|
||||||
ts *schema.TypeSystem
|
|
||||||
errSchema error
|
|
||||||
)
|
|
||||||
|
|
||||||
func argsType() schema.Type {
|
|
||||||
once.Do(func() {
|
|
||||||
ts, errSchema = ipld.LoadSchemaBytes([]byte(argsSchema))
|
|
||||||
})
|
|
||||||
if errSchema != nil {
|
|
||||||
panic(errSchema)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ts.TypeByName(argsName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func roundTripThroughDAGCBOR(t *testing.T, argsIn *args.Args) *args.Args {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
node, err := argsIn.ToIPLD()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
data, err := ipld.Encode(node, dagcbor.Encode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var argsOut args.Args
|
|
||||||
_, err = ipld.Unmarshal(data, dagcbor.Decode, &argsOut, argsType())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return &argsOut
|
|
||||||
}
|
|
||||||
|
|
||||||
func must[T any](t T, err error) T {
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package args
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Builder allows the fluid construction of an Args.
|
|
||||||
type Builder struct {
|
|
||||||
args *Args
|
|
||||||
errs error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBuilder returns a Builder which will assemble the Args.
|
|
||||||
func NewBuilder() *Builder {
|
|
||||||
return &Builder{
|
|
||||||
args: New(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add inserts a new key/val into the Args being assembled while collecting
|
|
||||||
// any errors caused by duplicate keys.
|
|
||||||
func (b *Builder) Add(key string, val any) *Builder {
|
|
||||||
b.errs = errors.Join(b.errs, b.args.Add(key, val))
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build returns the assembled Args or an error containing a list of
|
|
||||||
// errors encountered while trying to build the Args.
|
|
||||||
func (b *Builder) Build() (*Args, error) {
|
|
||||||
if b.errs != nil {
|
|
||||||
return nil, b.errs
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.args, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildIPLD is the same as Build except it takes the additional step of
|
|
||||||
// converting the Args to an ipld.Node.
|
|
||||||
func (b *Builder) BuildIPLD() (ipld.Node, error) {
|
|
||||||
args, err := b.Build()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return args.ToIPLD()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustBuild is the same as Build except it panics if an error occurs.
|
|
||||||
func (b *Builder) MustBuild() *Args {
|
|
||||||
args, err := b.Build()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(b.errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustBuildIPLD is the same as BuildIPLD except it panics if an error
|
|
||||||
// occurs.
|
|
||||||
func (b *Builder) MustBuildIPLD() ipld.Node {
|
|
||||||
node, err := b.BuildIPLD()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package args_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/args"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuilder_XXX(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
keyOne = "key1"
|
|
||||||
valOne = "string"
|
|
||||||
keyTwo = "key2"
|
|
||||||
valTwo = 42
|
|
||||||
)
|
|
||||||
|
|
||||||
exp := args.New()
|
|
||||||
exp.Add(keyOne, valOne)
|
|
||||||
exp.Add(keyTwo, valTwo)
|
|
||||||
|
|
||||||
expNode, err := exp.ToIPLD()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
disjointKeys := args.NewBuilder().
|
|
||||||
Add(keyOne, valOne).
|
|
||||||
Add(keyTwo, valTwo)
|
|
||||||
|
|
||||||
duplicateKeys := args.NewBuilder().
|
|
||||||
Add(keyOne, valOne).
|
|
||||||
Add(keyTwo, valTwo).
|
|
||||||
Add(keyOne, "oh no!")
|
|
||||||
|
|
||||||
t.Run("MustBuild succeeds with disjoint keys", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var act *args.Args
|
|
||||||
|
|
||||||
require.NotPanics(t, func() {
|
|
||||||
act = disjointKeys.MustBuild()
|
|
||||||
})
|
|
||||||
assert.Equal(t, exp, act)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("MustBuild fails with duplicate keys", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var act *args.Args
|
|
||||||
|
|
||||||
require.Panics(t, func() {
|
|
||||||
act = duplicateKeys.MustBuild()
|
|
||||||
})
|
|
||||||
assert.Nil(t, act)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("MustBuildIPLD succeeds with disjoint keys", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var act ipld.Node
|
|
||||||
|
|
||||||
require.NotPanics(t, func() {
|
|
||||||
act = disjointKeys.MustBuildIPLD()
|
|
||||||
})
|
|
||||||
assert.Equal(t, expNode, act)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("MustBuildIPLD fails with duplicate keys", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var act ipld.Node
|
|
||||||
|
|
||||||
require.Panics(t, func() {
|
|
||||||
act = duplicateKeys.MustBuildIPLD()
|
|
||||||
})
|
|
||||||
assert.Nil(t, act)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package args
|
|
||||||
|
|
||||||
import (
|
|
||||||
"iter"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ReadOnly struct {
|
|
||||||
args *Args
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
|
||||||
return r.args.GetNode(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) Len() int {
|
|
||||||
return r.args.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
|
||||||
return r.args.Iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) ToIPLD() (ipld.Node, error) {
|
|
||||||
return r.args.ToIPLD()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) Equals(other ReadOnly) bool {
|
|
||||||
return r.args.Equals(other.args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) String() string {
|
|
||||||
return r.args.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) WriteableClone() *Args {
|
|
||||||
return r.args.Clone()
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const separator = "/"
|
|
||||||
|
|
||||||
var _ fmt.Stringer = (*Command)(nil)
|
|
||||||
|
|
||||||
// Command is a concrete messages (a "verb") that MUST be unambiguously
|
|
||||||
// interpretable by the Subject of a UCAN.
|
|
||||||
//
|
|
||||||
// A [Command] is composed of a leading slash which is optionally followed
|
|
||||||
// by one or more slash-separated Segments of lowercase characters.
|
|
||||||
//
|
|
||||||
// [Command]: https://github.com/ucan-wg/spec#command
|
|
||||||
type Command string
|
|
||||||
|
|
||||||
// New creates a validated command from the provided list of segment strings.
|
|
||||||
// An error is returned if an invalid Command would be formed
|
|
||||||
func New(segments ...string) Command {
|
|
||||||
return Top().Join(segments...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse verifies that the provided string contains the required
|
|
||||||
// [segment structure] and, if valid, returns the resulting
|
|
||||||
// Command.
|
|
||||||
//
|
|
||||||
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
func Parse(s string) (Command, error) {
|
|
||||||
if !strings.HasPrefix(s, "/") {
|
|
||||||
return "", ErrRequiresLeadingSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
|
||||||
return "", ErrDisallowsTrailingSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
if s != strings.ToLower(s) {
|
|
||||||
return "", ErrRequiresLowercase
|
|
||||||
}
|
|
||||||
|
|
||||||
// The leading slash will result in the first element from strings.Split
|
|
||||||
// being an empty string which is removed as strings.Join will ignore it.
|
|
||||||
return Command(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustParse is the same as Parse, but panic() if the parsing fail.
|
|
||||||
func MustParse(s string) Command {
|
|
||||||
c, err := Parse(s)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top is the most powerful capability.
|
|
||||||
//
|
|
||||||
// This function returns a Command that is a wildcard and therefore represents the
|
|
||||||
// most powerful ability. As such, it should be handled with care and used sparingly.
|
|
||||||
//
|
|
||||||
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
|
||||||
func Top() Command {
|
|
||||||
return Command(separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid returns true if the provided string is a valid UCAN command.
|
|
||||||
func IsValid(s string) bool {
|
|
||||||
_, err := Parse(s)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join appends segments to the end of this command using the required
|
|
||||||
// segment separator.
|
|
||||||
func (c Command) Join(segments ...string) Command {
|
|
||||||
size := 0
|
|
||||||
for _, s := range segments {
|
|
||||||
size += len(s)
|
|
||||||
}
|
|
||||||
if size == 0 {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
buf := make([]byte, 0, len(c)+size+len(segments))
|
|
||||||
buf = append(buf, []byte(c)...)
|
|
||||||
for _, s := range segments {
|
|
||||||
if s != "" {
|
|
||||||
if len(buf) > 1 {
|
|
||||||
buf = append(buf, separator...)
|
|
||||||
}
|
|
||||||
buf = append(buf, []byte(s)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Command(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Segments returns the ordered segments that comprise the Command as a
|
|
||||||
// slice of strings.
|
|
||||||
func (c Command) Segments() []string {
|
|
||||||
if c == separator {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return strings.Split(string(c), separator)[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Covers returns true if the command is identical or a parent of the given other command.
|
|
||||||
func (c Command) Covers(other Command) bool {
|
|
||||||
// fast-path, equivalent to the code below (verified with fuzzing)
|
|
||||||
if !strings.HasPrefix(string(other), string(c)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return c == separator || len(c) == len(other) || other[len(c)] == separator[0]
|
|
||||||
|
|
||||||
/* -------
|
|
||||||
|
|
||||||
otherSegments := other.Segments()
|
|
||||||
if len(otherSegments) < len(c.Segments()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i, s := range c.Segments() {
|
|
||||||
if otherSegments[i] != s {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the composed representation the command. This is also
|
|
||||||
// the required wire representation (before IPLD encoding occurs.)
|
|
||||||
func (c Command) String() string {
|
|
||||||
return string(c)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// ErrRequiresLeadingSlash is returned when a parsing a string that
|
|
||||||
// doesn't start with a [leading slash character].
|
|
||||||
//
|
|
||||||
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrRequiresLeadingSlash = fmt.Errorf("a command requires a leading slash character")
|
|
||||||
|
|
||||||
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
|
|
||||||
// with a trailing slash character].
|
|
||||||
//
|
|
||||||
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrDisallowsTrailingSlash = fmt.Errorf("a command must not include a trailing slash")
|
|
||||||
|
|
||||||
// ErrRequiresLowercase is returned if a Command contains, or would contain,
|
|
||||||
// [uppercase unicode characters].
|
|
||||||
//
|
|
||||||
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrRequiresLowercase = fmt.Errorf("UCAN path segments must must not contain upper-case characters")
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# Token container
|
|
||||||
|
|
||||||
The specification has been promoted to https://github.com/ucan-wg/container.
|
|
||||||
|
|
||||||
## Why do I need that?
|
|
||||||
|
|
||||||
Some common situation asks to package multiple tokens together:
|
|
||||||
- calling a service requires sending an invocation, alongside the matching delegations
|
|
||||||
- sending a series of revocations
|
|
||||||
- \<insert your application specific scenario here>
|
|
||||||
|
|
||||||
The UCAN specification defines how a single token is serialized (envelope with signature, IPLD encoded as Dag-cbor), but it's entirely left open how to package multiple tokens together. To be clear, this is a correct thing to do for a specification, as different ways equally valid to solve that problem exists and can coexist. Any wire format holding a list of bytes would do (cbor, json, csv ...).
|
|
||||||
|
|
||||||
**go-ucan** however, provide an opinionated implementation, which may or may not work in your situation.
|
|
||||||
|
|
||||||
Some experiment has been done over which format is appropriate, and two have been selected:
|
|
||||||
- **DAG-CBOR** of a list of bytes, as a low overhead option
|
|
||||||
- **CAR** file, as a somewhat common ways to cary arbitrary blocks of data
|
|
||||||
|
|
||||||
Notably, **compression is not included**, even though it does work reasonably well. This is because your transport medium might already do it, or should.
|
|
||||||
|
|
||||||
## Wire format consideration
|
|
||||||
|
|
||||||
Several possible formats have been explored:
|
|
||||||
- CAR files (binary or base64)
|
|
||||||
- DAG-CBOR (binary or base64)
|
|
||||||
|
|
||||||
Additionally, gzip and deflate compression has been experimented with.
|
|
||||||
|
|
||||||
Below are the results in terms of storage used, as percentage and byte overhead over the raw tokens:
|
|
||||||
|
|
||||||
| Token count | car | carBase64 | carGzip | carGzipBase64 | cbor | cborBase64 | cborGzip | cborGzipBase64 | cborFlate | cborFlateBase64 |
|
|
||||||
|-------------|-----|-----------|---------|---------------|------|------------|----------|----------------|-----------|-----------------|
|
|
||||||
| 1 | 15 | 54 | 7 | 42 | 0 | 35 | \-8 | 22 | \-12 | 16 |
|
|
||||||
| 2 | 12 | 49 | \-12 | 15 | 0 | 34 | \-25 | 0 | \-28 | \-3 |
|
|
||||||
| 3 | 11 | 48 | \-21 | 4 | 0 | 34 | \-32 | \-10 | \-34 | \-11 |
|
|
||||||
| 4 | 10 | 47 | \-26 | \-1 | 0 | 34 | \-36 | \-15 | \-37 | \-17 |
|
|
||||||
| 5 | 10 | 47 | \-28 | \-4 | 0 | 34 | \-38 | \-18 | \-40 | \-20 |
|
|
||||||
| 6 | 10 | 47 | \-30 | \-7 | 0 | 34 | \-40 | \-20 | \-40 | \-20 |
|
|
||||||
| 7 | 10 | 46 | \-31 | \-8 | 0 | 34 | \-41 | \-21 | \-42 | \-22 |
|
|
||||||
| 8 | 9 | 46 | \-32 | \-10 | 0 | 34 | \-42 | \-22 | \-42 | \-23 |
|
|
||||||
| 9 | 9 | 46 | \-33 | \-11 | 0 | 34 | \-43 | \-23 | \-43 | \-24 |
|
|
||||||
| 10 | 9 | 46 | \-34 | \-12 | 0 | 34 | \-43 | \-25 | \-44 | \-25 |
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
| Token count | car | carBase64 | carGzip | carGzipBase64 | cbor | cborBase64 | cborGzip | cborGzipBase64 | cborFlate | cborFlateBase64 |
|
|
||||||
|-------------|-----|-----------|---------|---------------|------|------------|----------|----------------|-----------|-----------------|
|
|
||||||
| 1 | 64 | 226 | 29 | 178 | 4 | 146 | \-35 | 94 | \-52 | 70 |
|
|
||||||
| 2 | 102 | 412 | \-107 | 128 | 7 | 288 | \-211 | 0 | \-234 | \-32 |
|
|
||||||
| 3 | 140 | 602 | \-270 | 58 | 10 | 430 | \-405 | \-126 | \-429 | \-146 |
|
|
||||||
| 4 | 178 | 792 | \-432 | \-28 | 13 | 572 | \-602 | \-252 | \-617 | \-288 |
|
|
||||||
| 5 | 216 | 978 | \-582 | \-94 | 16 | 714 | \-805 | \-386 | \-839 | \-418 |
|
|
||||||
| 6 | 254 | 1168 | \-759 | \-176 | 19 | 856 | \-1001 | \-508 | \-1018 | \-520 |
|
|
||||||
| 7 | 292 | 1358 | \-908 | \-246 | 22 | 998 | \-1204 | \-634 | \-1229 | \-650 |
|
|
||||||
| 8 | 330 | 1544 | \-1085 | \-332 | 25 | 1140 | \-1398 | \-756 | \-1423 | \-792 |
|
|
||||||
| 9 | 368 | 1734 | \-1257 | \-414 | 28 | 1282 | \-1614 | \-894 | \-1625 | \-930 |
|
|
||||||
| 10 | 406 | 1924 | \-1408 | \-508 | 31 | 1424 | \-1804 | \-1040 | \-1826 | \-1060 |
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Following is the performance aspect, with CPU usage and memory allocation:
|
|
||||||
|
|
||||||
| | Write ns/op | Read ns/op | Write B/op | Read B/op | Write allocs/op | Read allocs/op |
|
|
||||||
|-----------------|-------------|------------|------------|-----------|-----------------|----------------|
|
|
||||||
| car | 8451 | 1474630 | 17928 | 149437 | 59 | 2631 |
|
|
||||||
| carBase64 | 16750 | 1437678 | 24232 | 151502 | 61 | 2633 |
|
|
||||||
| carGzip | 320253 | 1581412 | 823887 | 192272 | 76 | 2665 |
|
|
||||||
| carGzipBase64 | 343305 | 1486269 | 828782 | 198543 | 77 | 2669 |
|
|
||||||
| cbor | 6419 | 1301554 | 16368 | 138891 | 25 | 2534 |
|
|
||||||
| cborBase64 | 12860 | 1386728 | 20720 | 140962 | 26 | 2536 |
|
|
||||||
| cborGzip | 310106 | 1379146 | 822742 | 182003 | 42 | 2585 |
|
|
||||||
| cborGzipBase64 | 317001 | 1462548 | 827640 | 189283 | 43 | 2594 |
|
|
||||||
| cborFlate | 327112 | 1555007 | 822473 | 181537 | 40 | 2591 |
|
|
||||||
| cborFlateBase64 | 311276 | 1456562 | 826042 | 188665 | 41 | 2596 |
|
|
||||||
|
|
||||||
(BEWARE: logarithmic scale)
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Conclusion:
|
|
||||||
- CAR files are heavy for this usage, notably because they carry the CIDs of the tokens
|
|
||||||
- compression works quite well and warrants its usage even with a single token
|
|
||||||
- DAG-CBOR outperform CAR files everywhere, and comes with a tiny ~3 bytes per token overhead.
|
|
||||||
|
|
||||||
**Formats beside DAG-CBOR and CAR, with or without base64, have been removed. They are in the git history though.**
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"iter"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
Note: below is essentially a re-implementation of the CAR file v1 read and write.
|
|
||||||
This exists here for two reasons:
|
|
||||||
- go-car's API forces to go through an IPLD getter or through a blockstore API
|
|
||||||
- generally, go-car is a very complex and large dependency
|
|
||||||
*/
|
|
||||||
|
|
||||||
// EmptyCid is a "zero" Cid: zero-length "identity" multihash with "raw" codec
|
|
||||||
// It can be used to have at least one root in a CARv1 file (making it legal), yet
|
|
||||||
// denote that it can be ignored.
|
|
||||||
var EmptyCid = cid.MustParse([]byte{01, 55, 00, 00})
|
|
||||||
|
|
||||||
type carBlock struct {
|
|
||||||
c cid.Cid
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeCar writes a CARv1 file containing the blocks from the iterator.
|
|
||||||
// If no roots are provided, a single EmptyCid is used as root to make the file
|
|
||||||
// spec compliant.
|
|
||||||
func writeCar(w io.Writer, roots []cid.Cid, blocks iter.Seq2[carBlock, error]) error {
|
|
||||||
if len(roots) == 0 {
|
|
||||||
roots = []cid.Cid{EmptyCid}
|
|
||||||
}
|
|
||||||
h := carHeader{
|
|
||||||
Roots: roots,
|
|
||||||
Version: 1,
|
|
||||||
}
|
|
||||||
hb, err := h.Write()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = ldWrite(w, hb)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for block, err := range blocks {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = ldWrite(w, block.c.Bytes(), block.data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readCar reads a CARv1 file from the reader, and return a block iterator.
|
|
||||||
// Roots are ignored.
|
|
||||||
func readCar(r io.Reader) (roots []cid.Cid, blocks iter.Seq2[carBlock, error], err error) {
|
|
||||||
br := bufio.NewReader(r)
|
|
||||||
|
|
||||||
hb, err := ldRead(br)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
h, err := readHeader(hb)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if h.Version != 1 {
|
|
||||||
return nil, nil, fmt.Errorf("invalid car version: %d", h.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.Roots, func(yield func(block carBlock, err error) bool) {
|
|
||||||
for {
|
|
||||||
block, err := readBlock(br)
|
|
||||||
if err == io.EOF {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if !yield(carBlock{}, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !yield(block, nil) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readBlock reads a section from the reader and decode a (cid+data) block.
|
|
||||||
func readBlock(r *bufio.Reader) (carBlock, error) {
|
|
||||||
raw, err := ldRead(r)
|
|
||||||
if err != nil {
|
|
||||||
return carBlock{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, c, err := cid.CidFromReader(bytes.NewReader(raw))
|
|
||||||
if err != nil {
|
|
||||||
return carBlock{}, err
|
|
||||||
}
|
|
||||||
data := raw[n:]
|
|
||||||
|
|
||||||
// integrity check
|
|
||||||
hashed, err := c.Prefix().Sum(data)
|
|
||||||
if err != nil {
|
|
||||||
return carBlock{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hashed.Equals(c) {
|
|
||||||
return carBlock{}, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", c, hashed)
|
|
||||||
}
|
|
||||||
|
|
||||||
return carBlock{c: c, data: data}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxAllowedSectionSize dictates the maximum number of bytes that a CARv1 header
|
|
||||||
// or section is allowed to occupy without causing a decode to error.
|
|
||||||
// This cannot be supplied as an option, only adjusted as a global. You should
|
|
||||||
// use v2#NewReader instead since it allows for options to be passed in.
|
|
||||||
var maxAllowedSectionSize uint = 32 << 20 // 32MiB
|
|
||||||
|
|
||||||
// ldRead performs a length-delimited read of a section from the reader.
|
|
||||||
// A section is composed of an uint length followed by the data.
|
|
||||||
func ldRead(r *bufio.Reader) ([]byte, error) {
|
|
||||||
if _, err := r.Peek(1); err != nil { // no more blocks, likely clean io.EOF
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := binary.ReadUvarint(r)
|
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
return nil, io.ErrUnexpectedEOF // don't silently pretend this is a clean EOF
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if l == 0 {
|
|
||||||
return nil, fmt.Errorf("invalid zero size section")
|
|
||||||
}
|
|
||||||
|
|
||||||
if l > uint64(maxAllowedSectionSize) { // Don't OOM
|
|
||||||
return nil, fmt.Errorf("malformed car; header is bigger than MaxAllowedSectionSize")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, l)
|
|
||||||
if _, err := io.ReadFull(r, buf); err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
// we should be able to read the promised bytes, this is not normal
|
|
||||||
return nil, io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ldWrite performs a length-delimited write of a section on the writer.
|
|
||||||
// A section is composed of an uint length followed by the data.
|
|
||||||
func ldWrite(w io.Writer, d ...[]byte) error {
|
|
||||||
var sum uint64
|
|
||||||
for _, s := range d {
|
|
||||||
sum += uint64(len(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 8)
|
|
||||||
n := binary.PutUvarint(buf, sum)
|
|
||||||
_, err := w.Write(buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range d {
|
|
||||||
_, err = w.Write(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type carHeader struct {
|
|
||||||
Roots []cid.Cid
|
|
||||||
Version uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootsKey = "roots"
|
|
||||||
const versionKey = "version"
|
|
||||||
|
|
||||||
func readHeader(data []byte) (*carHeader, error) {
|
|
||||||
var header carHeader
|
|
||||||
|
|
||||||
nd, err := ipld.Decode(data, dagcbor.Decode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if nd.Length() != 2 {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
rootsNd, err := nd.LookupByString(rootsKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
it := rootsNd.ListIterator()
|
|
||||||
if it == nil {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
header.Roots = make([]cid.Cid, 0, rootsNd.Length())
|
|
||||||
for !it.Done() {
|
|
||||||
_, nd, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
lk, err := nd.AsLink()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch lk := lk.(type) {
|
|
||||||
case cidlink.Link:
|
|
||||||
header.Roots = append(header.Roots, lk.Cid)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
versionNd, err := nd.LookupByString(versionKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
version, err := versionNd.AsInt()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
header.Version = uint64(version)
|
|
||||||
return &header, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *carHeader) Write() ([]byte, error) {
|
|
||||||
nd, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
|
||||||
qp.MapEntry(ma, rootsKey, qp.List(int64(len(ch.Roots)), func(la datamodel.ListAssembler) {
|
|
||||||
for _, root := range ch.Roots {
|
|
||||||
qp.ListEntry(la, qp.Link(cidlink.Link{Cid: root}))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
qp.MapEntry(ma, versionKey, qp.Int(1))
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ipld.Encode(nd, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCarRoundTrip(t *testing.T) {
|
|
||||||
// this car file is a complex and legal CARv1 file
|
|
||||||
original, err := os.ReadFile("testdata/sample-v1.car")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
roots, it, err := readCar(bytes.NewReader(original))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var blks []carBlock
|
|
||||||
for blk, err := range it {
|
|
||||||
require.NoError(t, err)
|
|
||||||
blks = append(blks, blk)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Len(t, blks, 1049)
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
err = writeCar(buf, roots, func(yield func(carBlock, error) bool) {
|
|
||||||
for _, blk := range blks {
|
|
||||||
if !yield(blk, nil) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Bytes equal after the round-trip
|
|
||||||
require.Equal(t, original, buf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzCarRoundTrip(f *testing.F) {
|
|
||||||
// Note: this fuzzing is somewhat broken.
|
|
||||||
// After some time, the fuzzer discover that a varint can be serialized in different
|
|
||||||
// ways that lead to the same integer value. This means that the CAR format can have
|
|
||||||
// multiple legal binary representation for the exact same data, which is what we are
|
|
||||||
// trying to detect here. Ideally, the format would be stricter, but that's how things
|
|
||||||
// are.
|
|
||||||
|
|
||||||
example, err := os.ReadFile("testdata/sample-v1.car")
|
|
||||||
require.NoError(f, err)
|
|
||||||
|
|
||||||
f.Add(example)
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
|
||||||
roots, blocksIter, err := readCar(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
// skip invalid binary
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
// reading all the blocks, which force reading and verifying the full file
|
|
||||||
var blocks []carBlock
|
|
||||||
for block, err := range blocksIter {
|
|
||||||
if err != nil {
|
|
||||||
// error reading, invalid data
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
blocks = append(blocks, block)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = writeCar(&buf, roots, func(yield func(carBlock, error) bool) {
|
|
||||||
for _, blk := range blocks {
|
|
||||||
if !yield(blk, nil) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// test if the round-trip produce a byte-equal CAR
|
|
||||||
require.Equal(t, data, buf.Bytes())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
OH4sIAAAAAAAA/5zXa5MU1R3HcXYjIOAVMQZQUQElCGx3n9M3wLhzn8xl3ZnpuSLB0+ecnp6enft9VBJWjSAkxEVXkBUJAgbULTUq4gULlWhQiauilKiFgiaFoIhoCVFIpXY3VXm2s2/g++RTv+7/+bOGi+nZZfauaNOGzkgrmfzUPZ9cOm50aPP2jdPmvbb81I3vbtiTWHDmwAcZZt+W78+6vCU3Irb6u23LjnR37vp07d5Iz7nHX9+96OnXlsw8e9Oaj87agHQnbDradLTpolyhhFG6hXTEW9k5zBxmdh7PYf+CUYlUJZIgc5O0NrcueJOJrDlsLkKXwBnEYlciBgsAWyqFgdnpy6oxPWU3SCpQE0L+KI9xiugtWibToqI8ptXsJL2rZRtOFAr/3yxqAQfJqG6iS6561FqQsk7ZkecUt9cac9WSpOSGJb9d9njNsIqzmY4lt2HU0YHnLFh4G/qVNqeMOkq0GRdK6vCzJEWL6CFdALzMsQIyEAZUYjlW0nRJJAwPNN5QBY3lAAC8oKuYUsLKqqFRAau8RgGl6UwaU891vofXTUn5Lu58ZDzpRzqlf63e713VHbzpqg8X3iqFfzniqfGtdlx7DrzQfPbDT2RXHLnnX9e/fLJp/eFR27974d9/3HHJyusXt7+Zcoyd9uIPSzePGAZSho/biynJrhC3JrEIFeSUkOc9rmSbwWqlMKl5Wc2d5kRUz1iGjOTLxLwatBZVv9tdkWFIoylbDvjsjjbOXSt7JFhkXVQgBS5ZagSpgewAEkcYqFHAGSojIEJEyCNdFjmMoUoNBjEyIBJhNR0JjEQIkQ1WhiJCSMBkAMl427RzzztvjD8G9jP9SONm9dxw6dFNH78+uqfpgtRV3ZEfJ3jfbfvkw0MLr/J2r205cGjrvOVvdNaa4tPO7GpfMWXU12+8N4ELOK/tu2DJzrc7rztnGEglu0VM521lPl0LmqOuhBRMZ/KC1ahYRXdSUax5bM9nqlRtJ5nKUJFyLjtvMoMyF0yaCvlE0mGLI1MkAKGlFOAEEiaKOx0qUls4YKo3gNRIdgBJpIwIJcgaUCaaJmkaILosaAQLEm8IkgiIpAmY0xECDFUhNhCUiCxzGGgDSIuv+WnPyHPyh1f3XRDuR/p2VXRu29o7XKfEVTN6F/kfv2XLyyfI7vWT+27+NvDZoZk3L7jm+Jm3Os9qPbjd0/fNHdmtTcKnnfKMpnVLf977PTjw2djhfO7CAQLNKFVS89GU325j0/ZcO0ybc+4QnyhhUfTasSoVK1aHxxgqklZIB+yYOjhUZWvRrNNVjqlK1iFUTTZTwFOLpMopW6ZYyeZ03t8AUiPZwSVJjIoAgwxNBSzlOVVWdaRJVOAwMgQKMKQSFXmdcARSEUkGFVUWS4IApAGkhyb+bPHD1yg3jHt1m9KP1D7Z81H2oxu/WEaZY5NOPtrT8fQGafFXc2/7HNw+4q5/2vnn9yJ2zIQFm8907niQ+eKbg9Gde73li/RJnv3dpmV/rY8cBpKRC1lNpmBBL/uSFVd7RkplEuFSOJRzJ4rZTCwlxnRLu9RWZKNCdqhImbBIY6Ag2CxqWrbFcrYsiXj8SibSHpDEWtRjCmeor8x6VL8oNYDUSHZwSVDGEuVYQxR5ATAaAZqu8ghSGQGDhRLAgOVkrGPK8xLPA0OgqkqoIKqDS1o/ZdHt7ldffNPyt3un9yPN+MU73vKTuw8F1z17+fe+51Yefn+s5+rz1t0Fn36m9akHz7viifvOXV1r//KVWRvbn22euGD9LPjK3d0FX2zTe4vgyMuOjRsGkibmcKCuhMPhesxQirLVAkvAEtUDvmilYI5EjIALAIGIfCwcHfKSUgVH0JZJKW2yFpFs2GKEzdlstaLkk0otmw6l7YYt68rURDGXa2RJDWQHkJj/fus4jTeoTCiUACMLOsuyLIepZlABUchCIEm6jCmLeYE3VKgyPOE1QR1Amsif2hd9xD//84nTf92PVDgynzy//f4Pbthd2fXV6dZXxl7bd1oYX7LN61o7f/TyS0bfd2Tes4ww76Y9S5ofK+y7+bONZ59/ZzI/6qLN+49dvX7mlObhHA5Wtz9ZTHJVZ8hU8lS5rN+XUarhUpuiQFeUr+QqcrBsWKyBSMQ0VKR4oEYdqbqYj8slrlyzSL6YyezWqZoTSDgcr5hqCleL1jTBldUbQGokO4gkEkGDCBuiJsssAJKGdEEVMRUhMVRKOVZlBRHoIoZAZBneIAyrchpiODyA9OBLv5tv77LPvqK+98p+pMyOH1b2jFrW+vfukx1XjlkxsnjnNPWdW3t63dHO5nMegCO37Np74uSkNZ7I0i1be0949RWt5y/v/P29k17auiZ0r/PCYS0pqoZ8ArZhD1EVn+IuakF/LOV3hi1WSYmgSDEfzJjSrhCXrg75BE9mcEGwu4uGWrRWg1az3aaLDo56QnWTJeJ3pKqRUnsorNkKXgQaQGokO4DEU5lqiFCDZ6jGMzIiSBcow/EaRw3IExYQwkFGxwKHJIGTDBkwGtIAwoMn+CfL7+77urfL/UxtwS39SH+6sGvR/M3b7j489vTU1OwxHxxnp7rWHVx98fpL9ph/M/fEq1ue+e1N/xDGNz9xCzw8q3vyZT2jp46ePuP9mZvqo/bv+6llOP+kuMfscECbC5eddZJTfHlT2M5bk4GEYvWZS3mxElTCKdkUS8XjcMjXHUtLCaermiY4S2g8zpoiRqIcssplwuUlECoqeiXutikRACONXHcNZAeXpKqCqMmcgURW1lgsAUkHkGUxo6kGhYSIEksg0mXACEjloAGhCmRVZoXBJS2LGo9+PG7/LrnvXXc/0iOu5JnKZS0/9n65Za8n+d3jdfGtx5ZEVtktu9uPT1+9Z8J2T8fGrulTS5mltvn3H+xyHljjfODJHWt3LkycGfOYDYwaBlLF7ra4goKzXg2zyJXysnwlTKS8I2su5R12iiqkXKon4gb0ytyQ30nBONtWymUrZU/cUi9DLppTvJZglavadJxyF5N2wSSBkDNiVWONvJMayP5vSURSkcAYLBYBx0CEVF0SoarxSDQIzyFGRZJMdY2DHBEk2ZA5RmU4BjBwAOlopu2+lb0heeXJP9zxnwAAAP//V8QWnXsQAAA=
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
PH4sIAAAAAAAA_5zW65MU1f3HcS7-hB8igpfIgqUxCqLC0tfTfUADMzs7M87ObubaOzMEsM-lp6eb6Zmee49GvEKwVBQRgygIUVHLCwoqoJAAikCQaJRICCqFGPCCcXVFoaBMJbukKs929h94P3nV-ZzvSg2XrEkV9u7kwFW3J6bv7bj7tflfrjm4e8_grc7Dke4jPx6c_Mm87olH_O9Of35_59-X7z2xe056dKx7156F074aOXzCcemCRe-Y4T3n3v7SNV-fdd4qVfcLA48NPDbwArtYxqo1mcxJT2ebmWZmUgE3s09jtUxqMsmQKSZ1ptRBu5lF0WK75ZbawpVO25NW2rLtfgG5XFZKtlwBr1GPVfyKU9YjZS_BOEv0yVouNxmpBUxr-TH6osmv4Uyx-L9NM5PUWqLtmqTohbQnkWp1i5JlxiOWLxAFAdaJeU13Z0gwWsN2AOdzc267A6tz5uDmGTPvUH-pNVfUOWU6CBfLqP9ZkqUl9fc6w3KqRmTVUDWGB1BiiKoDCBhJptRABAuCxrJY0zHPQBZjbLCY8hKCVBOplbMwDXbdsO9O4ZT5m20fTPywBymh3HfTBjDl5MzlT65xpm15kC4-f_2hlTtKyQUbrj78wBPfjn1e3T9i7OmuEfmt25d4z_q2K_0H36lRiw_9NbK0Szy0d3A_kPKuFk6qOYqVTSCj2uGNknxdT0BN0Etlqklxr8QaSqzF468oQl-R0oo7rztKQaK5crqzmMq7vOV4uh4jFZ2zPIrfFL0hS_ZYOFikDSA1ku1FAqJEZIRUA2gMy0OeY4EOKGVlHmCDIgZKKhYhp2NKGQR5YvBYZAWVp4j0Io1b4N905xU_e_SJj1-4pAfpbefa207fnMit_9XP39xxw7D3Fk0bH7vs09W_3uwcG3Vk5Nxv14yYP2Zj7lLPQ01Nnw0bsmXTssuZprL6-dX3zB3U9NH9q8_tB1KhgwUG8gA54HH5OcMyW1vqjmLnO11GNB0Ph6ut8WI1zAbdSMv2FclKhWK2riCS9trFvDvmd3tC0bpo8SAeC5QExZfiO3LeIOuKePkGkBrJnkEiWJYlnjEg1nhERSKouowIUgEvGUTGRBN5Ims6QiyVZVUwNIp4GUNOxr1I199x4OH_y6103Q-3DOhBuhYw1x1Y9eHYjejREfahp2Z9SNaqo83Xb0yff-mFr3_srFg7at3FV2aEy28atv2p8zbc-313YOQ3y65gm1Z9MHzdipHm0H4gFYmaLrQGWjpFLp4ICmagamWKHI560tlAsoa1uNcotoQKWj7fGuvz3Ck1v5hJgvZQJGYIKTlcFrypWocVLHPuEC_oWa2e0M3OVsen-BuZuwayZ-YOckQARDJ4TLAsChqUdVHVOBkA1pA5GVKGkaiqA04CDISiIcoUMhRjCHuRmjJraxO7f_ji-PiTE3pf0g83F4Zc-hF4-7uTV4XPG7xw986TC__2_qlnjrf9OLDrrKfeXioPGvtN0-NHRv8OHh7_5fzP1V3Lt40Zar-z-c3Vtx5O54b0AykTCrGsVS1iKmkdapQ1q6lqKWkV2gD1J3MIY09SlHw5O6yHgn1FqnoTLU66YobsbASoKq46rmoka2c8LomNkHBFYH2VVldNa-GTQgNIjWR7kTiVAIYRsYEFTcMCgpDqogw4TFXGwKqmYaACRtMJ4ClmEDAQFaHAYAbSXqRXxt9z8fIVxXOXjpjI9SDtXIkK6MJ3T6xbv-WuN7azi9K8UGl5a1t056x9G6fgxJJ5g03lSBk_O2juXV0Lnh2kVHfIwcqrKw-9dboId5X-eU5__iRPLhyxMsF03C9ZnWYKtRfZajIbUDjb68k7XFG2UL5adKesKOwrku42qkgrRaK2lI1jYJaMelnpaLf9RhW0sfmUrjk-FymaAcHBDSA1ku1FYjVeBBpGBqUCpjIUKdI5TuAwwqqhiQAAyvAQ6BoDBCgiwSCizMsSQhzfi_SP-2K_-PGE9saxTzMze5Dy-z0P7Di679olRys3fzdIXbZgbpvy0Krm5oXnnP_1w9yzj0yF8_64bCjzW2H22feEv48NLk6_8UD32fWPNvlf_CS18OUB_UDSE1WcaNUKSsqx_SjA-zNeLYI4T0cybfM4pbMg4AknnbbOetjqK1K-blcKUt4DWcNrVBzBIiJ2fGE7W8gCKZbI4WhBy4KkP9MWhA0gNZL9LxJLVMQhA_KEkTAjEkbnBZFILMQGQRLPi5gSpAOqEShqqoFFWZA4jCHoRRLeO9XcMqtwy7p35cd6kIY99snirRBtnvqcHN9benz4hCuG3LJt9bafZki7thTP_mHpi5PGXfbY6N1Nm68Z992tLD51S-q5DR98dsnhAbNSNzwZvOr_-4FUTdpy0XIjlg0nZSHkqaneUBVUCylgZEtmwQ3LhZrhR3WtkrX7PHegSpISVKtKuCYFyu11s5pK6FHbR112BNtsIZUOt1fSHVIuLzYydw1kz8ydoGqUF4nBizIHZYFyog5ZLHCU0QwEOZGROVEiusr824blDCgBSSIMgGdO8ClPP7KYPJnsQIeKG3uQrnz5teFvvD98hrrOc3nrmK3rn9s_-09PjBzaddOwxDl3Hz1xfEx810uPhvYcbL3sL9dNWjnuxamjXnCFEheAP29i5nXf3tGf6y7r6qwrJZfHlLyGLCvuWCf05Qp6JGDYBaNWqnvS5bKN_VRBVaWvSDnJBG4lHBEAl05yIccVIHZabmmLBttdobSjaKY3FRY9alvY42oAqZFsL5KKCAaEQkMlokgpyzKijhlRpaIoGZClCLCU1ahOIGQhFWSDQwLLqkhlzrwkvOH-ScEtSw68Omr87B6kpYWRa185WbvuC8u8aMC0i1pXbHeO72P9982fsHhh2xbf8WGe0y5Zq-wAk3YenZ05evG93MzVAx78aXJlxr1r2hce7A9Srqh6jIIVjNU7INENUg7Tqmz7tFBb2pfy-oq6zxuOOO0kWo27-3yC58tCwi900DxOtpk-k2txFCfuCGKllnOscqVk-W0N1dmoFcw3coI3kO1FElVWYhiiGhzgCS-xPES6zFKVAZxgyIAXJZWjAOsygppAEG_8Z-wIQZzai7Ri5eyp3V8981nL9fET_woAAP__svLMp3sQAAA
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,21 +0,0 @@
|
|||||||
package containertest
|
|
||||||
|
|
||||||
import _ "embed"
|
|
||||||
|
|
||||||
//go:embed Base64StdPadding
|
|
||||||
var Base64StdPadding string
|
|
||||||
|
|
||||||
//go:embed Base64StdPaddingGzipped
|
|
||||||
var Base64StdPaddingGzipped string
|
|
||||||
|
|
||||||
//go:embed Base64URL
|
|
||||||
var Base64URL string
|
|
||||||
|
|
||||||
//go:embed Base64URLGzipped
|
|
||||||
var Base64URLGzipped string
|
|
||||||
|
|
||||||
//go:embed Bytes
|
|
||||||
var Bytes []byte
|
|
||||||
|
|
||||||
//go:embed BytesGzipped
|
|
||||||
var BytesGzipped []byte
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
@@ -1,118 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/gzip"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
const containerVersionTag = "ctn-v1"
|
|
||||||
|
|
||||||
type header byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
headerRawBytes = header(0x40)
|
|
||||||
headerBase64StdPadding = header(0x42)
|
|
||||||
headerBase64URL = header(0x43)
|
|
||||||
headerRawBytesGzip = header(0x4D)
|
|
||||||
headerBase64StdPaddingGzip = header(0x4F)
|
|
||||||
headerBase64URLGzip = header(0x50)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h header) encoder(w io.Writer) *payloadWriter {
|
|
||||||
res := &payloadWriter{rawWriter: w, writer: w, header: h}
|
|
||||||
|
|
||||||
switch h {
|
|
||||||
case headerBase64StdPadding, headerBase64StdPaddingGzip:
|
|
||||||
b64Writer := base64.NewEncoder(base64.StdEncoding, res.writer)
|
|
||||||
res.writer = b64Writer
|
|
||||||
res.closers = append([]io.Closer{b64Writer}, res.closers...)
|
|
||||||
case headerBase64URL, headerBase64URLGzip:
|
|
||||||
b64Writer := base64.NewEncoder(base64.RawURLEncoding, res.writer)
|
|
||||||
res.writer = b64Writer
|
|
||||||
res.closers = append([]io.Closer{b64Writer}, res.closers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch h {
|
|
||||||
case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip:
|
|
||||||
gzipWriter := gzip.NewWriter(res.writer)
|
|
||||||
res.writer = gzipWriter
|
|
||||||
res.closers = append([]io.Closer{gzipWriter}, res.closers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func payloadDecoder(r io.Reader) (io.Reader, error) {
|
|
||||||
headerBuf := make([]byte, 1)
|
|
||||||
_, err := r.Read(headerBuf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h := header(headerBuf[0])
|
|
||||||
|
|
||||||
switch h {
|
|
||||||
case headerRawBytes,
|
|
||||||
headerBase64StdPadding,
|
|
||||||
headerBase64URL,
|
|
||||||
headerRawBytesGzip,
|
|
||||||
headerBase64StdPaddingGzip,
|
|
||||||
headerBase64URLGzip:
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown container header")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch h {
|
|
||||||
case headerBase64StdPadding, headerBase64StdPaddingGzip:
|
|
||||||
r = base64.NewDecoder(base64.StdEncoding, r)
|
|
||||||
case headerBase64URL, headerBase64URLGzip:
|
|
||||||
r = base64.NewDecoder(base64.RawURLEncoding, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch h {
|
|
||||||
case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip:
|
|
||||||
gzipReader, err := gzip.NewReader(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
r = gzipReader
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ io.WriteCloser = &payloadWriter{}
|
|
||||||
|
|
||||||
// payloadWriter is tasked with two things:
|
|
||||||
// - prepend the header byte
|
|
||||||
// - call Close() on all the underlying io.Writer
|
|
||||||
type payloadWriter struct {
|
|
||||||
rawWriter io.Writer
|
|
||||||
writer io.Writer
|
|
||||||
header header
|
|
||||||
headerWrote bool
|
|
||||||
closers []io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *payloadWriter) Write(p []byte) (n int, err error) {
|
|
||||||
if !w.headerWrote {
|
|
||||||
_, err := w.rawWriter.Write([]byte{byte(w.header)})
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
w.headerWrote = true
|
|
||||||
}
|
|
||||||
return w.writer.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *payloadWriter) Close() error {
|
|
||||||
var errs error
|
|
||||||
for _, closer := range w.closers {
|
|
||||||
if err := closer.Close(); err != nil {
|
|
||||||
errs = errors.Join(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"iter"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/cbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/token"
|
|
||||||
"code.sonr.org/go/ucan/token/delegation"
|
|
||||||
"code.sonr.org/go/ucan/token/invocation"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrNotFound = fmt.Errorf("not found")
|
|
||||||
var ErrMultipleInvocations = fmt.Errorf("multiple invocations")
|
|
||||||
|
|
||||||
// Reader is a token container reader. It exposes the tokens conveniently decoded.
|
|
||||||
type Reader map[cid.Cid]bundle
|
|
||||||
|
|
||||||
type bundle struct {
|
|
||||||
sealed []byte
|
|
||||||
token token.Token
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromBytes decodes a container from a []byte
|
|
||||||
func FromBytes(data []byte) (Reader, error) {
|
|
||||||
return FromReader(bytes.NewReader(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromString decodes a container from a string
|
|
||||||
func FromString(s string) (Reader, error) {
|
|
||||||
return FromReader(strings.NewReader(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromReader decodes a container from an io.Reader.
|
|
||||||
func FromReader(r io.Reader) (Reader, error) {
|
|
||||||
payload, err := payloadDecoder(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := ipld.DecodeStreaming(payload, cbor.Decode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if n.Kind() != datamodel.Kind_Map {
|
|
||||||
return nil, fmt.Errorf("invalid container format: expected map")
|
|
||||||
}
|
|
||||||
if n.Length() != 1 {
|
|
||||||
return nil, fmt.Errorf("invalid container format: expected single version key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the first (and only) key-value pair
|
|
||||||
it := n.MapIterator()
|
|
||||||
key, tokensNode, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
version, err := key.AsString()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid container format: version must be string")
|
|
||||||
}
|
|
||||||
if version != containerVersionTag {
|
|
||||||
return nil, fmt.Errorf("unsupported container version: %s", version)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokensNode.Kind() != datamodel.Kind_List {
|
|
||||||
return nil, fmt.Errorf("invalid container format: tokens must be a list")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctn := make(Reader, tokensNode.Length())
|
|
||||||
it2 := tokensNode.ListIterator()
|
|
||||||
for !it2.Done() {
|
|
||||||
_, val, err := it2.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data, err := val.AsBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = ctn.addToken(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetToken returns an arbitrary decoded token, from its CID.
|
|
||||||
// If not found, ErrNotFound is returned.
|
|
||||||
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
|
|
||||||
bndl, ok := ctn[cid]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return bndl.token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSealed returns an arbitrary sealed token, from its CID.
|
|
||||||
// If not found, ErrNotFound is returned.
|
|
||||||
func (ctn Reader) GetSealed(cid cid.Cid) ([]byte, error) {
|
|
||||||
bndl, ok := ctn[cid]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return bndl.sealed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllTokens return all the tokens in the container.
|
|
||||||
func (ctn Reader) GetAllTokens() iter.Seq[token.Bundle] {
|
|
||||||
return func(yield func(token.Bundle) bool) {
|
|
||||||
for c, bndl := range ctn {
|
|
||||||
if !yield(token.Bundle{
|
|
||||||
Cid: c,
|
|
||||||
Decoded: bndl.token,
|
|
||||||
Sealed: bndl.sealed,
|
|
||||||
}) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
|
|
||||||
// If not found, delegation.ErrDelegationNotFound is returned.
|
|
||||||
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
|
||||||
tkn, err := ctn.GetToken(cid)
|
|
||||||
if err != nil { // only ErrNotFound expected
|
|
||||||
return nil, delegation.ErrDelegationNotFound
|
|
||||||
}
|
|
||||||
if tkn, ok := tkn.(*delegation.Token); ok {
|
|
||||||
return tkn, nil
|
|
||||||
}
|
|
||||||
return nil, delegation.ErrDelegationNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDelegationBundle is the same as GetToken but only return a delegation.Bundle, with the right type.
|
|
||||||
// If not found, delegation.ErrDelegationNotFound is returned.
|
|
||||||
func (ctn Reader) GetDelegationBundle(cid cid.Cid) (*delegation.Bundle, error) {
|
|
||||||
bndl, ok := ctn[cid]
|
|
||||||
if !ok {
|
|
||||||
return nil, delegation.ErrDelegationNotFound
|
|
||||||
}
|
|
||||||
if tkn, ok := bndl.token.(*delegation.Token); ok {
|
|
||||||
return &delegation.Bundle{
|
|
||||||
Cid: cid,
|
|
||||||
Decoded: tkn,
|
|
||||||
Sealed: bndl.sealed,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, delegation.ErrDelegationNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllDelegations returns all the delegation.Token in the container.
|
|
||||||
func (ctn Reader) GetAllDelegations() iter.Seq[*delegation.Bundle] {
|
|
||||||
return func(yield func(*delegation.Bundle) bool) {
|
|
||||||
for c, bndl := range ctn {
|
|
||||||
if t, ok := bndl.token.(*delegation.Token); ok {
|
|
||||||
if !yield(&delegation.Bundle{
|
|
||||||
Cid: c,
|
|
||||||
Decoded: t,
|
|
||||||
Sealed: bndl.sealed,
|
|
||||||
}) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInvocation returns a single invocation.Token.
|
|
||||||
// If none are found, ErrNotFound is returned.
|
|
||||||
// If more than one invocation exists, ErrMultipleInvocations is returned.
|
|
||||||
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
|
||||||
var res *invocation.Token
|
|
||||||
for _, bndl := range ctn {
|
|
||||||
if inv, ok := bndl.token.(*invocation.Token); ok {
|
|
||||||
if res != nil {
|
|
||||||
return nil, ErrMultipleInvocations
|
|
||||||
}
|
|
||||||
res = inv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if res == nil {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllInvocations returns all the invocation.Token in the container.
|
|
||||||
func (ctn Reader) GetAllInvocations() iter.Seq[invocation.Bundle] {
|
|
||||||
return func(yield func(invocation.Bundle) bool) {
|
|
||||||
for c, bndl := range ctn {
|
|
||||||
if t, ok := bndl.token.(*invocation.Token); ok {
|
|
||||||
if !yield(invocation.Bundle{
|
|
||||||
Cid: c,
|
|
||||||
Decoded: t,
|
|
||||||
Sealed: bndl.sealed,
|
|
||||||
}) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Reader) addToken(data []byte) error {
|
|
||||||
tkn, c, err := token.FromSealed(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ctn[c] = bundle{
|
|
||||||
sealed: data,
|
|
||||||
token: tkn,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToWriter convert a container Reader into a Writer.
|
|
||||||
// Most likely, you only want to use this in tests for convenience.
|
|
||||||
func (ctn Reader) ToWriter() Writer {
|
|
||||||
writer := NewWriter()
|
|
||||||
for _, bndl := range ctn {
|
|
||||||
writer.AddSealed(bndl.sealed)
|
|
||||||
}
|
|
||||||
return writer
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.sonr.org/go/did-it"
|
|
||||||
"code.sonr.org/go/did-it/controller/did-key"
|
|
||||||
"code.sonr.org/go/did-it/crypto/ed25519"
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/command"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/literal"
|
|
||||||
"code.sonr.org/go/ucan/token/delegation"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContainerRoundTrip(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
expectedHeader header
|
|
||||||
writer any
|
|
||||||
}{
|
|
||||||
{"Bytes", headerRawBytes, Writer.ToBytes},
|
|
||||||
{"BytesWriter", headerRawBytes, Writer.ToBytesWriter},
|
|
||||||
{"BytesGzipped", headerRawBytesGzip, Writer.ToBytesGzipped},
|
|
||||||
{"BytesGzippedWriter", headerRawBytesGzip, Writer.ToBytesGzippedWriter},
|
|
||||||
{"Base64StdPadding", headerBase64StdPadding, Writer.ToBase64StdPadding},
|
|
||||||
{"Base64StdPaddingWriter", headerBase64StdPadding, Writer.ToBase64StdPaddingWriter},
|
|
||||||
{"Base64StdPaddingGzipped", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzipped},
|
|
||||||
{"Base64StdPaddingGzippedWriter", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzippedWriter},
|
|
||||||
{"Base64URL", headerBase64URL, Writer.ToBase64URL},
|
|
||||||
{"Base64URLWriter", headerBase64URL, Writer.ToBase64URLWriter},
|
|
||||||
{"Base64URLGzipped", headerBase64URLGzip, Writer.ToBase64URLGzipped},
|
|
||||||
{"Base64URLGzipWriter", headerBase64URLGzip, Writer.ToBase64URLGzipWriter},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
tokens := make(map[cid.Cid]*delegation.Token)
|
|
||||||
var dataSize int
|
|
||||||
|
|
||||||
writer := NewWriter()
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
dlg, c, data := randToken()
|
|
||||||
writer.AddSealed(data)
|
|
||||||
tokens[c] = dlg
|
|
||||||
dataSize += len(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
var reader Reader
|
|
||||||
var serialLen int
|
|
||||||
|
|
||||||
switch fn := tc.writer.(type) {
|
|
||||||
case func(ctn Writer, w io.Writer) error:
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
err := fn(writer, buf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
serialLen = buf.Len()
|
|
||||||
|
|
||||||
h, err := buf.ReadByte()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, byte(tc.expectedHeader), h)
|
|
||||||
err = buf.UnreadByte()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
reader, err = FromReader(bytes.NewReader(buf.Bytes()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
case func(ctn Writer) ([]byte, error):
|
|
||||||
b, err := fn(writer)
|
|
||||||
require.NoError(t, err)
|
|
||||||
serialLen = len(b)
|
|
||||||
|
|
||||||
require.Equal(t, byte(tc.expectedHeader), b[0])
|
|
||||||
|
|
||||||
reader, err = FromBytes(b)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
case func(ctn Writer) (string, error):
|
|
||||||
s, err := fn(writer)
|
|
||||||
require.NoError(t, err)
|
|
||||||
serialLen = len(s)
|
|
||||||
|
|
||||||
require.Equal(t, byte(tc.expectedHeader), s[0])
|
|
||||||
|
|
||||||
reader, err = FromString(s)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("data size %d, container size %d, overhead: %d%%, %d bytes",
|
|
||||||
dataSize, serialLen, int(float32(serialLen-dataSize)/float32(dataSize)*100.0), serialLen-dataSize)
|
|
||||||
|
|
||||||
for c, dlg := range tokens {
|
|
||||||
tknRead, err := reader.GetToken(c)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// require.Equal fails as time.Time holds a wall time that is going to be
|
|
||||||
// different, even if it represents the same event.
|
|
||||||
// We need to do the following instead.
|
|
||||||
|
|
||||||
dlgRead := tknRead.(*delegation.Token)
|
|
||||||
require.Equal(t, dlg.Issuer(), dlgRead.Issuer())
|
|
||||||
require.Equal(t, dlg.Audience(), dlgRead.Audience())
|
|
||||||
require.Equal(t, dlg.Subject(), dlgRead.Subject())
|
|
||||||
require.Equal(t, dlg.Command(), dlgRead.Command())
|
|
||||||
require.Equal(t, dlg.Policy(), dlgRead.Policy())
|
|
||||||
require.Equal(t, dlg.Nonce(), dlgRead.Nonce())
|
|
||||||
require.True(t, dlg.Meta().Equals(dlgRead.Meta()))
|
|
||||||
if dlg.NotBefore() != nil {
|
|
||||||
// within 1s as the original value gets truncated to seconds when serialized
|
|
||||||
require.WithinDuration(t, *dlg.NotBefore(), *dlgRead.NotBefore(), time.Second)
|
|
||||||
}
|
|
||||||
if dlg.Expiration() != nil {
|
|
||||||
// within 1s as the original value gets truncated to seconds when serialized
|
|
||||||
require.WithinDuration(t, *dlg.Expiration(), *dlgRead.Expiration(), time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkContainerSerialisation(b *testing.B) {
|
|
||||||
var duration strings.Builder
|
|
||||||
var allocByte strings.Builder
|
|
||||||
var allocCount strings.Builder
|
|
||||||
|
|
||||||
for _, builder := range []strings.Builder{duration, allocByte, allocCount} {
|
|
||||||
builder.WriteString("car\tcarBase64\tcarGzip\tcarGzipBase64\tcbor\tcborBase64\tcborGzip\tcborGzipBase64\tcborFlate\tcborFlateBase64\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
writer func(ctn Writer, w io.Writer) error
|
|
||||||
reader func(io.Reader) (Reader, error)
|
|
||||||
}{
|
|
||||||
{"Bytes", Writer.ToBytesWriter, FromReader},
|
|
||||||
{"BytesGzipped", Writer.ToBytesGzippedWriter, FromReader},
|
|
||||||
{"Base64StdPadding", Writer.ToBase64StdPaddingWriter, FromReader},
|
|
||||||
{"Base64StdPaddingGzipped", Writer.ToBase64StdPaddingGzippedWriter, FromReader},
|
|
||||||
{"Base64URL", Writer.ToBase64URLWriter, FromReader},
|
|
||||||
{"Base64URLGzip", Writer.ToBase64URLGzipWriter, FromReader},
|
|
||||||
} {
|
|
||||||
writer := NewWriter()
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
_, _, data := randToken()
|
|
||||||
writer.AddSealed(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
_ = tc.writer(writer, buf)
|
|
||||||
|
|
||||||
b.Run(tc.name+"_write", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
_ = tc.writer(writer, buf)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run(tc.name+"_read", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = tc.reader(bytes.NewReader(buf.Bytes()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func randDID() (ed25519.PrivateKey, did.DID) {
|
|
||||||
_, privKey, err := ed25519.GenerateKeyPair()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
d := didkeyctl.FromPrivateKey(privKey)
|
|
||||||
return privKey, d
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomString(length int) string {
|
|
||||||
b := make([]byte, length/2+1)
|
|
||||||
_, _ = rand.Read(b)
|
|
||||||
return fmt.Sprintf("%x", b)[0:length]
|
|
||||||
}
|
|
||||||
|
|
||||||
func randToken() (*delegation.Token, cid.Cid, []byte) {
|
|
||||||
priv, iss := randDID()
|
|
||||||
_, aud := randDID()
|
|
||||||
cmd := command.New("foo", "bar")
|
|
||||||
pol := policy.MustConstruct(
|
|
||||||
policy.All(".[]",
|
|
||||||
policy.GreaterThan(".value", literal.Int(2)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
opts := []delegation.Option{
|
|
||||||
delegation.WithExpiration(time.Now().Add(time.Hour)),
|
|
||||||
}
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := delegation.Root(iss, aud, cmd, pol, opts...)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
b, c, err := t.ToSealed(priv)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return t, c, b
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzContainerRead(f *testing.F) {
|
|
||||||
// Generate a corpus
|
|
||||||
for tokenCount := 0; tokenCount < 10; tokenCount++ {
|
|
||||||
writer := NewWriter()
|
|
||||||
for i := 0; i < tokenCount; i++ {
|
|
||||||
_, _, data := randToken()
|
|
||||||
writer.AddSealed(data)
|
|
||||||
}
|
|
||||||
data, err := writer.ToBytes()
|
|
||||||
require.NoError(f, err)
|
|
||||||
|
|
||||||
f.Add(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
// search for panics
|
|
||||||
_, _ = FromBytes(data)
|
|
||||||
|
|
||||||
if time.Since(start) > 100*time.Millisecond {
|
|
||||||
panic("too long")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
BIN
pkg/container/testdata/sample-v1.car
vendored
BIN
pkg/container/testdata/sample-v1.car
vendored
Binary file not shown.
@@ -1,145 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/cbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
|
|
||||||
type Writer map[string]struct{}
|
|
||||||
|
|
||||||
func NewWriter() Writer {
|
|
||||||
return make(Writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddSealed includes a "sealed" token (serialized with a ToSealed* function) in the container.
|
|
||||||
func (ctn Writer) AddSealed(data []byte) {
|
|
||||||
ctn[string(data)] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBytes encode the container into raw bytes.
|
|
||||||
func (ctn Writer) ToBytes() ([]byte, error) {
|
|
||||||
return ctn.toBytes(headerRawBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBytesWriter is the same as ToBytes, but with an io.Writer.
|
|
||||||
func (ctn Writer) ToBytesWriter(w io.Writer) error {
|
|
||||||
return ctn.toWriter(headerRawBytes, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBytesGzipped encode the container into gzipped bytes.
|
|
||||||
func (ctn Writer) ToBytesGzipped() ([]byte, error) {
|
|
||||||
return ctn.toBytes(headerRawBytesGzip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBytesGzippedWriter is the same as ToBytesGzipped, but with an io.Writer.
|
|
||||||
func (ctn Writer) ToBytesGzippedWriter(w io.Writer) error {
|
|
||||||
return ctn.toWriter(headerRawBytesGzip, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBase64StdPadding encode the container into a base64 string, with standard encoding and padding.
|
|
||||||
func (ctn Writer) ToBase64StdPadding() (string, error) {
|
|
||||||
return ctn.toString(headerBase64StdPadding)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBase64StdPaddingWriter is the same as ToBase64StdPadding, but with an io.Writer.
|
|
||||||
func (ctn Writer) ToBase64StdPaddingWriter(w io.Writer) error {
|
|
||||||
return ctn.toWriter(headerBase64StdPadding, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBase64StdPaddingGzipped encode the container into a pre-gzipped base64 string, with standard encoding and padding.
|
|
||||||
func (ctn Writer) ToBase64StdPaddingGzipped() (string, error) {
|
|
||||||
return ctn.toString(headerBase64StdPaddingGzip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBase64StdPaddingGzippedWriter is the same as ToBase64StdPaddingGzipped, but with an io.Writer.
|
|
||||||
func (ctn Writer) ToBase64StdPaddingGzippedWriter(w io.Writer) error {
|
|
||||||
return ctn.toWriter(headerBase64StdPaddingGzip, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBase64URL encode the container into base64 string, with URL-safe encoding and no padding.
|
|
||||||
func (ctn Writer) ToBase64URL() (string, error) {
|
|
||||||
return ctn.toString(headerBase64URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer.
|
|
||||||
func (ctn Writer) ToBase64URLWriter(w io.Writer) error {
|
|
||||||
return ctn.toWriter(headerBase64URL, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBase64URLGzipped encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding.
|
|
||||||
func (ctn Writer) ToBase64URLGzipped() (string, error) {
|
|
||||||
return ctn.toString(headerBase64URLGzip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBase64URLGzipWriter is the same as ToBase64URL, but with an io.Writer.
|
|
||||||
func (ctn Writer) ToBase64URLGzipWriter(w io.Writer) error {
|
|
||||||
return ctn.toWriter(headerBase64URLGzip, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Writer) toBytes(header header) ([]byte, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err := ctn.toWriter(header, &buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Writer) toString(header header) (string, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err := ctn.toWriter(header, &buf)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Writer) toWriter(header header, w io.Writer) (err error) {
|
|
||||||
encoder := header.encoder(w)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err = encoder.Close()
|
|
||||||
}()
|
|
||||||
node, err := qp.BuildMap(basicnode.Prototype.Any, 1, func(ma datamodel.MapAssembler) {
|
|
||||||
qp.MapEntry(ma, containerVersionTag, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) {
|
|
||||||
tokens := make([][]byte, 0, len(ctn))
|
|
||||||
for data := range ctn {
|
|
||||||
tokens = append(tokens, []byte(data))
|
|
||||||
}
|
|
||||||
slices.SortFunc(tokens, bytes.Compare)
|
|
||||||
for _, data := range tokens {
|
|
||||||
qp.ListEntry(la, qp.Bytes(data))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipld.EncodeStreaming(encoder, node, cbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToReader convert a container Writer into a Reader.
|
|
||||||
// Most likely, you only want to use this in tests for convenience.
|
|
||||||
// This is not optimized and can panic.
|
|
||||||
func (ctn Writer) ToReader() Reader {
|
|
||||||
data, err := ctn.ToBytes()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := FromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWriterDedup(t *testing.T) {
|
|
||||||
ctn := NewWriter()
|
|
||||||
|
|
||||||
_, _, sealed := randToken()
|
|
||||||
ctn.AddSealed(sealed)
|
|
||||||
require.Len(t, ctn, 1)
|
|
||||||
|
|
||||||
ctn.AddSealed(sealed)
|
|
||||||
require.Len(t, ctn, 1)
|
|
||||||
}
|
|
||||||
265
pkg/meta/meta.go
265
pkg/meta/meta.go
@@ -1,265 +0,0 @@
|
|||||||
package meta
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"iter"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/printer"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/literal"
|
|
||||||
"code.sonr.org/go/ucan/pkg/secretbox"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrNotFound = errors.New("key not found in meta")
|
|
||||||
|
|
||||||
var ErrNotEncryptable = errors.New("value of this type cannot be encrypted")
|
|
||||||
|
|
||||||
// Meta is a container for meta key-value pairs in a UCAN token.
|
|
||||||
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
|
||||||
// and transformations, while hiding the IPLD complexity from the caller.
|
|
||||||
type Meta struct {
|
|
||||||
// This type must be compatible with the IPLD type represented by the IPLD
|
|
||||||
// schema { String : Any }.
|
|
||||||
|
|
||||||
Keys []string
|
|
||||||
Values map[string]ipld.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMeta constructs a new Meta.
|
|
||||||
func NewMeta() *Meta {
|
|
||||||
return &Meta{Values: map[string]ipld.Node{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBool retrieves a value as a bool.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetBool(key string) (bool, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return false, ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsBool()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetString retrieves a value as a string.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetString(key string) (string, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return "", ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEncryptedString decorates GetString and decrypt its output with the given symmetric encryption key.
|
|
||||||
func (m *Meta) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
|
||||||
v, err := m.GetBytes(key)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypted, err := secretbox.DecryptStringWithKey(v, encryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(decrypted), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInt64 retrieves a value as an int64.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetInt64(key string) (int64, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return 0, ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFloat64 retrieves a value as a float64.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetFloat64(key string) (float64, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return 0, ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBytes retrieves a value as a []byte.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetBytes(key string) ([]byte, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEncryptedBytes decorates GetBytes and decrypt its output with the given symmetric encryption key.
|
|
||||||
func (m *Meta) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
|
||||||
v, err := m.GetBytes(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypted, err := secretbox.DecryptStringWithKey(v, encryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return decrypted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode retrieves a value as a raw IPLD node.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
func (m *Meta) GetNode(key string) (ipld.Node, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a key/value pair in the meta set.
|
|
||||||
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
|
||||||
func (m *Meta) Add(key string, val any) error {
|
|
||||||
if _, ok := m.Values[key]; ok {
|
|
||||||
return fmt.Errorf("duplicate key %q", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
node, err := literal.Any(val)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Keys = append(m.Keys, key)
|
|
||||||
m.Values[key] = node
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddEncrypted adds a key/value pair in the meta set.
|
|
||||||
// The value is encrypted with the given encryptionKey.
|
|
||||||
// Accepted types for the value are: string, []byte.
|
|
||||||
// The ciphertext will be 40 bytes larger than the plaintext due to encryption overhead.
|
|
||||||
func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error {
|
|
||||||
var encrypted []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch val := val.(type) {
|
|
||||||
case string:
|
|
||||||
encrypted, err = secretbox.EncryptWithKey([]byte(val), encryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case []byte:
|
|
||||||
encrypted, err = secretbox.EncryptWithKey(val, encryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return ErrNotEncryptable
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.Add(key, encrypted)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Iterator interface {
|
|
||||||
Iter() iter.Seq2[string, ipld.Node]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include merges the provided meta into the existing one.
|
|
||||||
//
|
|
||||||
// If duplicate keys are encountered, the new value is silently dropped
|
|
||||||
// without causing an error.
|
|
||||||
func (m *Meta) Include(other Iterator) {
|
|
||||||
for key, value := range other.Iter() {
|
|
||||||
if _, ok := m.Values[key]; ok {
|
|
||||||
// don't overwrite
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.Values[key] = value
|
|
||||||
m.Keys = append(m.Keys, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of key/values.
|
|
||||||
func (m *Meta) Len() int {
|
|
||||||
return len(m.Values)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iter iterates over the meta key/values
|
|
||||||
func (m *Meta) Iter() iter.Seq2[string, ipld.Node] {
|
|
||||||
return func(yield func(string, ipld.Node) bool) {
|
|
||||||
for _, key := range m.Keys {
|
|
||||||
if !yield(key, m.Values[key]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals tells if two Meta hold the same key/values.
|
|
||||||
func (m *Meta) Equals(other *Meta) bool {
|
|
||||||
if len(m.Keys) != len(other.Keys) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(m.Values) != len(other.Values) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, key := range m.Keys {
|
|
||||||
if !ipld.DeepEqual(m.Values[key], other.Values[key]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Meta) String() string {
|
|
||||||
sort.Strings(m.Keys)
|
|
||||||
|
|
||||||
buf := strings.Builder{}
|
|
||||||
buf.WriteString("{")
|
|
||||||
|
|
||||||
for key, node := range m.Values {
|
|
||||||
buf.WriteString("\n\t")
|
|
||||||
buf.WriteString(key)
|
|
||||||
buf.WriteString(": ")
|
|
||||||
buf.WriteString(strings.ReplaceAll(printer.Sprint(node), "\n", "\n\t"))
|
|
||||||
buf.WriteString(",")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.Values) > 0 {
|
|
||||||
buf.WriteString("\n")
|
|
||||||
}
|
|
||||||
buf.WriteString("}")
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadOnly returns a read-only version of Meta.
|
|
||||||
func (m *Meta) ReadOnly() ReadOnly {
|
|
||||||
return ReadOnly{meta: m}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone makes a deep copy.
|
|
||||||
func (m *Meta) Clone() *Meta {
|
|
||||||
res := &Meta{
|
|
||||||
Keys: make([]string, len(m.Keys)),
|
|
||||||
Values: make(map[string]ipld.Node, len(m.Values)),
|
|
||||||
}
|
|
||||||
copy(res.Keys, m.Keys)
|
|
||||||
for k, v := range m.Values {
|
|
||||||
res.Values[k] = v
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package meta_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"maps"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/meta"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMeta_Add(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
type Unsupported struct{}
|
|
||||||
|
|
||||||
t.Run("error if not primitive or Node", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("encrypted meta", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
key := make([]byte, 32)
|
|
||||||
_, err := rand.Read(key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
m := meta.NewMeta()
|
|
||||||
|
|
||||||
// string encryption
|
|
||||||
err = m.AddEncrypted("secret", "hello world", key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = m.GetString("secret")
|
|
||||||
require.Error(t, err) // the ciphertext is saved as []byte instead of string
|
|
||||||
|
|
||||||
decrypted, err := m.GetEncryptedString("secret", key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "hello world", decrypted)
|
|
||||||
|
|
||||||
// bytes encryption
|
|
||||||
originalBytes := make([]byte, 128)
|
|
||||||
_, err = rand.Read(originalBytes)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = m.AddEncrypted("secret-bytes", originalBytes, key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
encryptedBytes, err := m.GetBytes("secret-bytes")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEqual(t, originalBytes, encryptedBytes)
|
|
||||||
|
|
||||||
decryptedBytes, err := m.GetEncryptedBytes("secret-bytes", key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, originalBytes, decryptedBytes)
|
|
||||||
|
|
||||||
// error cases
|
|
||||||
t.Run("error on unsupported type", func(t *testing.T) {
|
|
||||||
err := m.AddEncrypted("invalid", 123, key)
|
|
||||||
require.ErrorIs(t, err, meta.ErrNotEncryptable)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("error on invalid key size", func(t *testing.T) {
|
|
||||||
err := m.AddEncrypted("invalid", "test", []byte("short-key"))
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "invalid key size")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("error on nil key", func(t *testing.T) {
|
|
||||||
err := m.AddEncrypted("invalid", "test", nil)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "encryption key is required")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIterCloneEquals(t *testing.T) {
|
|
||||||
m := meta.NewMeta()
|
|
||||||
|
|
||||||
require.NoError(t, m.Add("foo", "bar"))
|
|
||||||
require.NoError(t, m.Add("baz", 1234))
|
|
||||||
|
|
||||||
expected := map[string]ipld.Node{
|
|
||||||
"foo": basicnode.NewString("bar"),
|
|
||||||
"baz": basicnode.NewInt(1234),
|
|
||||||
}
|
|
||||||
|
|
||||||
// meta -> iter
|
|
||||||
require.Equal(t, expected, maps.Collect(m.Iter()))
|
|
||||||
|
|
||||||
// readonly -> iter
|
|
||||||
ro := m.ReadOnly()
|
|
||||||
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
|
||||||
|
|
||||||
// meta -> clone -> iter
|
|
||||||
clone := m.Clone()
|
|
||||||
require.Equal(t, expected, maps.Collect(clone.Iter()))
|
|
||||||
|
|
||||||
// readonly -> WriteableClone -> iter
|
|
||||||
wclone := ro.WriteableClone()
|
|
||||||
require.Equal(t, expected, maps.Collect(wclone.Iter()))
|
|
||||||
|
|
||||||
require.True(t, m.Equals(wclone))
|
|
||||||
require.True(t, ro.Equals(wclone.ReadOnly()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInclude(t *testing.T) {
|
|
||||||
m1 := meta.NewMeta()
|
|
||||||
|
|
||||||
require.NoError(t, m1.Add("samekey", "bar"))
|
|
||||||
require.NoError(t, m1.Add("baz", 1234))
|
|
||||||
|
|
||||||
m2 := meta.NewMeta()
|
|
||||||
|
|
||||||
require.NoError(t, m2.Add("samekey", "othervalue")) // check no overwrite
|
|
||||||
require.NoError(t, m2.Add("otherkey", 1234))
|
|
||||||
|
|
||||||
m1.Include(m2)
|
|
||||||
|
|
||||||
require.Equal(t, map[string]ipld.Node{
|
|
||||||
"samekey": basicnode.NewString("bar"),
|
|
||||||
"baz": basicnode.NewInt(1234),
|
|
||||||
"otherkey": basicnode.NewInt(1234),
|
|
||||||
}, maps.Collect(m1.Iter()))
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package meta
|
|
||||||
|
|
||||||
import (
|
|
||||||
"iter"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadOnly wraps a Meta into a read-only facade.
|
|
||||||
type ReadOnly struct {
|
|
||||||
meta *Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetBool(key string) (bool, error) {
|
|
||||||
return r.meta.GetBool(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetString(key string) (string, error) {
|
|
||||||
return r.meta.GetString(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
|
||||||
return r.meta.GetEncryptedString(key, encryptionKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
|
||||||
return r.meta.GetInt64(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetFloat64(key string) (float64, error) {
|
|
||||||
return r.meta.GetFloat64(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetBytes(key string) ([]byte, error) {
|
|
||||||
return r.meta.GetBytes(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
|
||||||
return r.meta.GetEncryptedBytes(key, encryptionKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
|
||||||
return r.meta.GetNode(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) Len() int {
|
|
||||||
return r.meta.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
|
||||||
return r.meta.Iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) Equals(other ReadOnly) bool {
|
|
||||||
return r.meta.Equals(other.meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) String() string {
|
|
||||||
return r.meta.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReadOnly) WriteableClone() *Meta {
|
|
||||||
return r.meta.Clone()
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
type glob string
|
|
||||||
|
|
||||||
// parseGlob ensures that the pattern conforms to the spec: only '*' and escaped '\*' are allowed.
|
|
||||||
func parseGlob(pattern string) (glob, error) {
|
|
||||||
for i := 0; i < len(pattern); i++ {
|
|
||||||
if pattern[i] == '*' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == '*' {
|
|
||||||
i++ // skip the escaped '*'
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pattern[i] == '\\' && i+1 < len(pattern) {
|
|
||||||
i++ // skip the escaped character
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pattern[i] == '\\' {
|
|
||||||
return "", fmt.Errorf("invalid escape sequence")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return glob(pattern), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustParseGlob(pattern string) glob {
|
|
||||||
g, err := parseGlob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return g
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match matches a string against the glob pattern with * wildcards, handling escaped '\*' literals.
|
|
||||||
func (pattern glob) Match(str string) bool {
|
|
||||||
// i is the index for the pattern
|
|
||||||
// j is the index for the string
|
|
||||||
var i, j int
|
|
||||||
|
|
||||||
// starIdx keeps track of the position of the last * in the pattern.
|
|
||||||
// matchIdx keeps track of the position in the string where the last * matched.
|
|
||||||
var starIdx, matchIdx int = -1, -1
|
|
||||||
|
|
||||||
for j < len(str) {
|
|
||||||
if i < len(pattern) && (pattern[i] == str[j] || pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == str[j]) {
|
|
||||||
// characters match or if there's an escaped character that matches
|
|
||||||
if pattern[i] == '\\' {
|
|
||||||
// skip the escape character
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
j++
|
|
||||||
} else if i < len(pattern) && pattern[i] == '*' {
|
|
||||||
// there's a * wildcard in the pattern
|
|
||||||
starIdx = i
|
|
||||||
matchIdx = j
|
|
||||||
i++
|
|
||||||
} else if starIdx != -1 {
|
|
||||||
// there's a previous * wildcard, backtrack
|
|
||||||
i = starIdx + 1
|
|
||||||
matchIdx++
|
|
||||||
j = matchIdx
|
|
||||||
} else {
|
|
||||||
// no match found
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for remaining characters in the pattern
|
|
||||||
for i < len(pattern) && pattern[i] == '*' {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
// the entire pattern is processed, it's a match
|
|
||||||
return i == len(pattern)
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSimpleGlobMatch(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
pattern string
|
|
||||||
str string
|
|
||||||
matches bool
|
|
||||||
}{
|
|
||||||
// Basic matching
|
|
||||||
{"*", "anything", true},
|
|
||||||
{"a*", "abc", true},
|
|
||||||
{"*c", "abc", true},
|
|
||||||
{"a*c", "abc", true},
|
|
||||||
{"a*c", "abxc", true},
|
|
||||||
{"a*c", "ac", true},
|
|
||||||
{"a*c", "a", false},
|
|
||||||
{"a*c", "ab", false},
|
|
||||||
|
|
||||||
// Escaped characters
|
|
||||||
{"a\\*c", "a*c", true},
|
|
||||||
{"a\\*c", "abc", false},
|
|
||||||
|
|
||||||
// Mixed wildcards and literals
|
|
||||||
{"a*b*c", "abc", true},
|
|
||||||
{"a*b*c", "aXbYc", true},
|
|
||||||
{"a*b*c", "aXbY", false},
|
|
||||||
{"a*b*c", "abYc", true},
|
|
||||||
{"a*b*c", "aXbc", true},
|
|
||||||
{"a*b*c", "aXbYcZ", false},
|
|
||||||
|
|
||||||
// Edge cases
|
|
||||||
{"", "", true},
|
|
||||||
{"", "a", false},
|
|
||||||
{"*", "", true},
|
|
||||||
{"*", "a", true},
|
|
||||||
{"\\*", "*", true},
|
|
||||||
{"\\*", "a", false},
|
|
||||||
|
|
||||||
// Specified test cases
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol.", true},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Dan, Erin, Carol.", true},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob , Carol.", true},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol.", true},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol", false},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol!", false},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice, Bob, Carol.", false},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice Cooper, Bob, Carol.", false},
|
|
||||||
{"Alice\\*, Bob*, Carol.", " Alice*, Bob, Carol. ", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.pattern+"_"+tt.str, func(t *testing.T) {
|
|
||||||
g, err := parseGlob(tt.pattern)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tt.matches, g.Match(tt.str))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkGlob(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
g := mustParseGlob("Alice\\*, Bob*, Carol.")
|
|
||||||
g.Match("Alice*, Bob*, Carol!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package limits
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/must"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// MaxInt53 represents the maximum safe integer in JavaScript (2^53 - 1)
|
|
||||||
MaxInt53 int64 = 9007199254740991
|
|
||||||
// MinInt53 represents the minimum safe integer in JavaScript (-2^53 + 1)
|
|
||||||
MinInt53 int64 = -9007199254740991
|
|
||||||
)
|
|
||||||
|
|
||||||
func ValidateIntegerBoundsIPLD(node ipld.Node) error {
|
|
||||||
switch node.Kind() {
|
|
||||||
case ipld.Kind_Int:
|
|
||||||
val := must.Int(node)
|
|
||||||
if val > MaxInt53 || val < MinInt53 {
|
|
||||||
return fmt.Errorf("integer value %d exceeds safe bounds", val)
|
|
||||||
}
|
|
||||||
case ipld.Kind_List:
|
|
||||||
it := node.ListIterator()
|
|
||||||
for !it.Done() {
|
|
||||||
_, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := ValidateIntegerBoundsIPLD(v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case ipld.Kind_Map:
|
|
||||||
it := node.MapIterator()
|
|
||||||
for !it.Done() {
|
|
||||||
_, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := ValidateIntegerBoundsIPLD(v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package limits
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateIntegerBoundsIPLD(t *testing.T) {
|
|
||||||
buildMap := func() datamodel.Node {
|
|
||||||
nb := basicnode.Prototype.Any.NewBuilder()
|
|
||||||
qp.Map(1, func(ma datamodel.MapAssembler) {
|
|
||||||
qp.MapEntry(ma, "foo", qp.Int(MaxInt53+1))
|
|
||||||
})(nb)
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
buildList := func() datamodel.Node {
|
|
||||||
nb := basicnode.Prototype.Any.NewBuilder()
|
|
||||||
qp.List(1, func(la datamodel.ListAssembler) {
|
|
||||||
qp.ListEntry(la, qp.Int(MinInt53-1))
|
|
||||||
})(nb)
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input datamodel.Node
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid int",
|
|
||||||
input: basicnode.NewInt(42),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "max safe int",
|
|
||||||
input: basicnode.NewInt(MaxInt53),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "min safe int",
|
|
||||||
input: basicnode.NewInt(MinInt53),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "above MaxInt53",
|
|
||||||
input: basicnode.NewInt(MaxInt53 + 1),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "below MinInt53",
|
|
||||||
input: basicnode.NewInt(MinInt53 - 1),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nested map with invalid int",
|
|
||||||
input: buildMap(),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nested list with invalid int",
|
|
||||||
input: buildList(),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
err := ValidateIntegerBoundsIPLD(tt.input)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "exceeds safe bounds")
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
// Package literal holds a collection of functions to create IPLD types to use in policies, selector and args.
|
|
||||||
package literal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/limits"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Bool = basicnode.NewBool
|
|
||||||
var Int = basicnode.NewInt
|
|
||||||
var Float = basicnode.NewFloat
|
|
||||||
var String = basicnode.NewString
|
|
||||||
var Bytes = basicnode.NewBytes
|
|
||||||
var Link = basicnode.NewLink
|
|
||||||
|
|
||||||
func LinkCid(cid cid.Cid) ipld.Node {
|
|
||||||
return Link(cidlink.Link{Cid: cid})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Null() ipld.Node {
|
|
||||||
nb := basicnode.Prototype.Any.NewBuilder()
|
|
||||||
nb.AssignNull()
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map creates an IPLD node from a map[string]any
|
|
||||||
func Map[T any](m map[string]T) (ipld.Node, error) {
|
|
||||||
return qp.BuildMap(basicnode.Prototype.Any, int64(len(m)), func(ma datamodel.MapAssembler) {
|
|
||||||
// deterministic iteration
|
|
||||||
keys := make([]string, 0, len(m))
|
|
||||||
for key := range m {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, key := range keys {
|
|
||||||
qp.MapEntry(ma, key, anyAssemble(m[key]))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// List creates an IPLD node from a []any
|
|
||||||
func List[T any](l []T) (ipld.Node, error) {
|
|
||||||
return qp.BuildList(basicnode.Prototype.Any, int64(len(l)), func(la datamodel.ListAssembler) {
|
|
||||||
for _, val := range l {
|
|
||||||
qp.ListEntry(la, anyAssemble(val))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any creates an IPLD node from any value
|
|
||||||
// If possible, use another dedicated function for your type for performance.
|
|
||||||
func Any(v any) (res ipld.Node, err error) {
|
|
||||||
// some fast path
|
|
||||||
switch val := v.(type) {
|
|
||||||
case bool:
|
|
||||||
return basicnode.NewBool(val), nil
|
|
||||||
case string:
|
|
||||||
return basicnode.NewString(val), nil
|
|
||||||
case int:
|
|
||||||
i := int64(val)
|
|
||||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
|
||||||
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", i)
|
|
||||||
}
|
|
||||||
return basicnode.NewInt(i), nil
|
|
||||||
case int8:
|
|
||||||
return basicnode.NewInt(int64(val)), nil
|
|
||||||
case int16:
|
|
||||||
return basicnode.NewInt(int64(val)), nil
|
|
||||||
case int32:
|
|
||||||
return basicnode.NewInt(int64(val)), nil
|
|
||||||
case int64:
|
|
||||||
if val > limits.MaxInt53 || val < limits.MinInt53 {
|
|
||||||
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", val)
|
|
||||||
}
|
|
||||||
return basicnode.NewInt(val), nil
|
|
||||||
case uint:
|
|
||||||
return basicnode.NewInt(int64(val)), nil
|
|
||||||
case uint8:
|
|
||||||
return basicnode.NewInt(int64(val)), nil
|
|
||||||
case uint16:
|
|
||||||
return basicnode.NewInt(int64(val)), nil
|
|
||||||
case uint32:
|
|
||||||
return basicnode.NewInt(int64(val)), nil
|
|
||||||
case uint64:
|
|
||||||
if val > uint64(limits.MaxInt53) {
|
|
||||||
return nil, fmt.Errorf("unsigned integer value %d exceeds safe integer bounds", val)
|
|
||||||
}
|
|
||||||
return basicnode.NewInt(int64(val)), nil
|
|
||||||
case float32:
|
|
||||||
return basicnode.NewFloat(float64(val)), nil
|
|
||||||
case float64:
|
|
||||||
return basicnode.NewFloat(val), nil
|
|
||||||
case []byte:
|
|
||||||
return basicnode.NewBytes(val), nil
|
|
||||||
case datamodel.Node:
|
|
||||||
return val, nil
|
|
||||||
case cid.Cid:
|
|
||||||
return LinkCid(val), nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := basicnode.Prototype__Any{}.NewBuilder()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
err = fmt.Errorf("%v", r)
|
|
||||||
res = nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
anyAssemble(v)(builder)
|
|
||||||
|
|
||||||
return builder.Build(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func anyAssemble(val any) qp.Assemble {
|
|
||||||
var rt reflect.Type
|
|
||||||
var rv reflect.Value
|
|
||||||
|
|
||||||
// support for recursive calls, staying in reflection land
|
|
||||||
if cast, ok := val.(reflect.Value); ok {
|
|
||||||
rt = cast.Type()
|
|
||||||
rv = cast
|
|
||||||
} else {
|
|
||||||
rt = reflect.TypeOf(val)
|
|
||||||
rv = reflect.ValueOf(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to dereference in some cases, to get the real value type
|
|
||||||
if rt.Kind() == reflect.Ptr || rt.Kind() == reflect.Interface {
|
|
||||||
rv = rv.Elem()
|
|
||||||
rt = rv.Type()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch rt.Kind() {
|
|
||||||
case reflect.Array:
|
|
||||||
if rt.Elem().Kind() == reflect.Uint8 {
|
|
||||||
panic("bytes array are not supported yet")
|
|
||||||
}
|
|
||||||
return qp.List(int64(rv.Len()), func(la datamodel.ListAssembler) {
|
|
||||||
for i := range rv.Len() {
|
|
||||||
qp.ListEntry(la, anyAssemble(rv.Index(i)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
case reflect.Slice:
|
|
||||||
if rt.Elem().Kind() == reflect.Uint8 {
|
|
||||||
return qp.Bytes(val.([]byte))
|
|
||||||
}
|
|
||||||
return qp.List(int64(rv.Len()), func(la datamodel.ListAssembler) {
|
|
||||||
for i := range rv.Len() {
|
|
||||||
qp.ListEntry(la, anyAssemble(rv.Index(i)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
case reflect.Map:
|
|
||||||
if rt.Key().Kind() != reflect.String {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// deterministic iteration
|
|
||||||
keys := rv.MapKeys()
|
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
|
||||||
return keys[i].String() < keys[j].String()
|
|
||||||
})
|
|
||||||
return qp.Map(int64(rv.Len()), func(ma datamodel.MapAssembler) {
|
|
||||||
for _, key := range keys {
|
|
||||||
qp.MapEntry(ma, key.String(), anyAssemble(rv.MapIndex(key)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
case reflect.Bool:
|
|
||||||
return qp.Bool(rv.Bool())
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
i := rv.Int()
|
|
||||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
|
||||||
panic(fmt.Sprintf("integer %d exceeds safe bounds", i))
|
|
||||||
}
|
|
||||||
return qp.Int(i)
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
u := rv.Uint()
|
|
||||||
if u > uint64(limits.MaxInt53) {
|
|
||||||
panic(fmt.Sprintf("unsigned integer %d exceeds safe bounds", u))
|
|
||||||
}
|
|
||||||
return qp.Int(int64(u))
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return qp.Float(rv.Float())
|
|
||||||
case reflect.String:
|
|
||||||
return qp.String(rv.String())
|
|
||||||
case reflect.Struct:
|
|
||||||
if rt == reflect.TypeOf(cid.Cid{}) {
|
|
||||||
c := rv.Interface().(cid.Cid)
|
|
||||||
return qp.Link(cidlink.Link{Cid: c})
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unsupported type %T", val))
|
|
||||||
}
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
package literal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
|
||||||
"github.com/ipld/go-ipld-prime/printer"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/limits"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestList(t *testing.T) {
|
|
||||||
n, err := List([]int{1, 2, 3})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_List, n.Kind())
|
|
||||||
require.Equal(t, int64(3), n.Length())
|
|
||||||
require.Equal(t, `list{
|
|
||||||
0: int{1}
|
|
||||||
1: int{2}
|
|
||||||
2: int{3}
|
|
||||||
}`, printer.Sprint(n))
|
|
||||||
|
|
||||||
n, err = List([][]int{{1, 2, 3}, {4, 5, 6}})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_List, n.Kind())
|
|
||||||
require.Equal(t, int64(2), n.Length())
|
|
||||||
require.Equal(t, `list{
|
|
||||||
0: list{
|
|
||||||
0: int{1}
|
|
||||||
1: int{2}
|
|
||||||
2: int{3}
|
|
||||||
}
|
|
||||||
1: list{
|
|
||||||
0: int{4}
|
|
||||||
1: int{5}
|
|
||||||
2: int{6}
|
|
||||||
}
|
|
||||||
}`, printer.Sprint(n))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMap(t *testing.T) {
|
|
||||||
n, err := Map(map[string]any{
|
|
||||||
"bool": true,
|
|
||||||
"string": "foobar",
|
|
||||||
"bytes": []byte{1, 2, 3, 4},
|
|
||||||
"int": 1234,
|
|
||||||
"uint": uint(12345),
|
|
||||||
"float": 1.45,
|
|
||||||
"slice": []int{1, 2, 3},
|
|
||||||
"array": [2]int{1, 2},
|
|
||||||
"map": map[string]any{
|
|
||||||
"foo": "bar",
|
|
||||||
"foofoo": map[string]string{
|
|
||||||
"barbar": "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
v, err := n.LookupByString("bool")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Bool, v.Kind())
|
|
||||||
require.Equal(t, true, must(v.AsBool()))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("string")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_String, v.Kind())
|
|
||||||
require.Equal(t, "foobar", must(v.AsString()))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("bytes")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Bytes, v.Kind())
|
|
||||||
require.Equal(t, []byte{1, 2, 3, 4}, must(v.AsBytes()))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("int")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
|
||||||
require.Equal(t, int64(1234), must(v.AsInt()))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("uint")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
|
||||||
require.Equal(t, int64(12345), must(v.AsInt()))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("float")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Float, v.Kind())
|
|
||||||
require.Equal(t, 1.45, must(v.AsFloat()))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("slice")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_List, v.Kind())
|
|
||||||
require.Equal(t, int64(3), v.Length())
|
|
||||||
require.Equal(t, `list{
|
|
||||||
0: int{1}
|
|
||||||
1: int{2}
|
|
||||||
2: int{3}
|
|
||||||
}`, printer.Sprint(v))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("array")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_List, v.Kind())
|
|
||||||
require.Equal(t, int64(2), v.Length())
|
|
||||||
require.Equal(t, `list{
|
|
||||||
0: int{1}
|
|
||||||
1: int{2}
|
|
||||||
}`, printer.Sprint(v))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("map")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Map, v.Kind())
|
|
||||||
require.Equal(t, int64(2), v.Length())
|
|
||||||
require.Equal(t, `map{
|
|
||||||
string{"foo"}: string{"bar"}
|
|
||||||
string{"foofoo"}: map{
|
|
||||||
string{"barbar"}: string{"foo"}
|
|
||||||
}
|
|
||||||
}`, printer.Sprint(v))
|
|
||||||
|
|
||||||
v, err = n.LookupByString("link")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Link, v.Kind())
|
|
||||||
asLink, err := v.AsLink()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAny(t *testing.T) {
|
|
||||||
data := map[string]any{
|
|
||||||
"bool": true,
|
|
||||||
"string": "foobar",
|
|
||||||
"bytes": []byte{1, 2, 3, 4},
|
|
||||||
"int": 1234,
|
|
||||||
"uint": uint(12345),
|
|
||||||
"float": 1.45,
|
|
||||||
"slice": []int{1, 2, 3},
|
|
||||||
"array": [2]int{1, 2},
|
|
||||||
"map": map[string]any{
|
|
||||||
"foo": "bar",
|
|
||||||
"foofoo": map[string]string{
|
|
||||||
"barbar": "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
|
||||||
"func": func() {},
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := Any(data["bool"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Bool, v.Kind())
|
|
||||||
require.Equal(t, true, must(v.AsBool()))
|
|
||||||
|
|
||||||
v, err = Any(data["string"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_String, v.Kind())
|
|
||||||
require.Equal(t, "foobar", must(v.AsString()))
|
|
||||||
|
|
||||||
v, err = Any(data["bytes"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Bytes, v.Kind())
|
|
||||||
require.Equal(t, []byte{1, 2, 3, 4}, must(v.AsBytes()))
|
|
||||||
|
|
||||||
v, err = Any(data["int"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
|
||||||
require.Equal(t, int64(1234), must(v.AsInt()))
|
|
||||||
|
|
||||||
v, err = Any(data["uint"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
|
||||||
require.Equal(t, int64(12345), must(v.AsInt()))
|
|
||||||
|
|
||||||
v, err = Any(data["float"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Float, v.Kind())
|
|
||||||
require.Equal(t, 1.45, must(v.AsFloat()))
|
|
||||||
|
|
||||||
v, err = Any(data["slice"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_List, v.Kind())
|
|
||||||
require.Equal(t, int64(3), v.Length())
|
|
||||||
require.Equal(t, `list{
|
|
||||||
0: int{1}
|
|
||||||
1: int{2}
|
|
||||||
2: int{3}
|
|
||||||
}`, printer.Sprint(v))
|
|
||||||
|
|
||||||
v, err = Any(data["array"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_List, v.Kind())
|
|
||||||
require.Equal(t, int64(2), v.Length())
|
|
||||||
require.Equal(t, `list{
|
|
||||||
0: int{1}
|
|
||||||
1: int{2}
|
|
||||||
}`, printer.Sprint(v))
|
|
||||||
|
|
||||||
v, err = Any(data["map"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Map, v.Kind())
|
|
||||||
require.Equal(t, int64(2), v.Length())
|
|
||||||
require.Equal(t, `map{
|
|
||||||
string{"foo"}: string{"bar"}
|
|
||||||
string{"foofoo"}: map{
|
|
||||||
string{"barbar"}: string{"foo"}
|
|
||||||
}
|
|
||||||
}`, printer.Sprint(v))
|
|
||||||
|
|
||||||
v, err = Any(data["link"])
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, datamodel.Kind_Link, v.Kind())
|
|
||||||
asLink, err := v.AsLink()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
|
||||||
|
|
||||||
_, err = Any(data["func"])
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAny(b *testing.B) {
|
|
||||||
b.Run("bool", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
_, _ = Any(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("string", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
_, _ = Any("foobar")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("bytes", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
_, _ = Any([]byte{1, 2, 3, 4})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("map", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
_, _ = Any(map[string]any{
|
|
||||||
"foo": "bar",
|
|
||||||
"foofoo": map[string]string{
|
|
||||||
"barbar": "foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnyAssembleIntegerOverflow(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input interface{}
|
|
||||||
shouldErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid int",
|
|
||||||
input: 42,
|
|
||||||
shouldErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "max safe int",
|
|
||||||
input: limits.MaxInt53,
|
|
||||||
shouldErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "min safe int",
|
|
||||||
input: limits.MinInt53,
|
|
||||||
shouldErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "overflow int",
|
|
||||||
input: int64(limits.MaxInt53 + 1),
|
|
||||||
shouldErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "underflow int",
|
|
||||||
input: int64(limits.MinInt53 - 1),
|
|
||||||
shouldErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "overflow uint",
|
|
||||||
input: uint64(limits.MaxInt53 + 1),
|
|
||||||
shouldErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
_, err := Any(tt.input)
|
|
||||||
if tt.shouldErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func must[T any](t T, err error) T {
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/must"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Match determines if the IPLD node satisfies the policy.
|
|
||||||
// The first Statement failing to match is returned as well.
|
|
||||||
func (p Policy) Match(node datamodel.Node) (bool, Statement) {
|
|
||||||
for _, stmt := range p {
|
|
||||||
res, leaf := matchStatement(stmt, node)
|
|
||||||
switch res {
|
|
||||||
case matchResultNoData, matchResultFalse:
|
|
||||||
return false, leaf
|
|
||||||
case matchResultOptionalNoData, matchResultTrue:
|
|
||||||
// continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PartialMatch returns false IIF one non-optional Statement has the corresponding data and doesn't match.
|
|
||||||
// If the data is missing or the non-optional Statement is matching, true is returned.
|
|
||||||
//
|
|
||||||
// This allows performing the policy checking in multiple steps, and find immediately if a Statement already failed.
|
|
||||||
// A final call to Match is necessary to make sure that the policy is fully matched, with no missing data
|
|
||||||
// (apart from optional values).
|
|
||||||
//
|
|
||||||
// The first Statement failing to match is returned as well.
|
|
||||||
func (p Policy) PartialMatch(node datamodel.Node) (bool, Statement) {
|
|
||||||
for _, stmt := range p {
|
|
||||||
res, leaf := matchStatement(stmt, node)
|
|
||||||
switch res {
|
|
||||||
case matchResultFalse:
|
|
||||||
return false, leaf
|
|
||||||
case matchResultNoData, matchResultOptionalNoData, matchResultTrue:
|
|
||||||
// continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type matchResult int8
|
|
||||||
|
|
||||||
const (
|
|
||||||
matchResultTrue matchResult = iota // statement has data and resolve to true
|
|
||||||
matchResultFalse // statement has data and resolve to false
|
|
||||||
matchResultNoData // statement has no data
|
|
||||||
matchResultOptionalNoData // statement has no data and is optional
|
|
||||||
)
|
|
||||||
|
|
||||||
// matchStatement evaluate the policy against the given ipld.Node and returns:
|
|
||||||
// - matchResultTrue: if the selector matched and the statement evaluated to true.
|
|
||||||
// - matchResultFalse: if the selector matched and the statement evaluated to false.
|
|
||||||
// - matchResultNoData: if the selector didn't match the expected data.
|
|
||||||
// For matchResultTrue and matchResultNoData, the leaf-most (innermost) statement failing to be true is returned,
|
|
||||||
// as well as the corresponding root-most encompassing statement.
|
|
||||||
func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Statement) {
|
|
||||||
var boolToRes = func(v bool) (matchResult, Statement) {
|
|
||||||
if v {
|
|
||||||
return matchResultTrue, nil
|
|
||||||
} else {
|
|
||||||
return matchResultFalse, cur
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cur.Kind() {
|
|
||||||
case KindEqual:
|
|
||||||
if s, ok := cur.(equality); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil { // optional selector didn't match
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
return boolToRes(datamodel.DeepEqual(s.value, res))
|
|
||||||
}
|
|
||||||
case KindNotEqual:
|
|
||||||
if s, ok := cur.(equality); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil { // optional selector didn't match
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
return boolToRes(!datamodel.DeepEqual(s.value, res))
|
|
||||||
}
|
|
||||||
case KindGreaterThan:
|
|
||||||
if s, ok := cur.(equality); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil { // optional selector didn't match
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
return boolToRes(isOrdered(s.value, res, gt))
|
|
||||||
}
|
|
||||||
case KindGreaterThanOrEqual:
|
|
||||||
if s, ok := cur.(equality); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil { // optional selector didn't match
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
return boolToRes(isOrdered(s.value, res, gte))
|
|
||||||
}
|
|
||||||
case KindLessThan:
|
|
||||||
if s, ok := cur.(equality); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil { // optional selector didn't match
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
return boolToRes(isOrdered(s.value, res, lt))
|
|
||||||
}
|
|
||||||
case KindLessThanOrEqual:
|
|
||||||
if s, ok := cur.(equality); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil { // optional selector didn't match
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
return boolToRes(isOrdered(s.value, res, lte))
|
|
||||||
}
|
|
||||||
case KindNot:
|
|
||||||
if s, ok := cur.(negation); ok {
|
|
||||||
res, leaf := matchStatement(s.statement, node)
|
|
||||||
switch res {
|
|
||||||
case matchResultNoData, matchResultOptionalNoData:
|
|
||||||
return res, leaf
|
|
||||||
case matchResultTrue:
|
|
||||||
return matchResultFalse, cur
|
|
||||||
case matchResultFalse:
|
|
||||||
return matchResultTrue, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case KindAnd:
|
|
||||||
if s, ok := cur.(connective); ok {
|
|
||||||
for _, cs := range s.statements {
|
|
||||||
res, leaf := matchStatement(cs, node)
|
|
||||||
switch res {
|
|
||||||
case matchResultNoData, matchResultOptionalNoData:
|
|
||||||
return res, leaf
|
|
||||||
case matchResultTrue:
|
|
||||||
// continue
|
|
||||||
case matchResultFalse:
|
|
||||||
return matchResultFalse, leaf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchResultTrue, nil
|
|
||||||
}
|
|
||||||
case KindOr:
|
|
||||||
if s, ok := cur.(connective); ok {
|
|
||||||
if len(s.statements) == 0 {
|
|
||||||
return matchResultTrue, nil
|
|
||||||
}
|
|
||||||
for _, cs := range s.statements {
|
|
||||||
res, leaf := matchStatement(cs, node)
|
|
||||||
switch res {
|
|
||||||
case matchResultNoData, matchResultOptionalNoData:
|
|
||||||
return res, leaf
|
|
||||||
case matchResultTrue:
|
|
||||||
return matchResultTrue, leaf
|
|
||||||
case matchResultFalse:
|
|
||||||
// continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchResultFalse, cur
|
|
||||||
}
|
|
||||||
case KindLike:
|
|
||||||
if s, ok := cur.(wildcard); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil { // optional selector didn't match
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
v, err := res.AsString()
|
|
||||||
if err != nil {
|
|
||||||
return matchResultFalse, cur // not a string
|
|
||||||
}
|
|
||||||
return boolToRes(s.pattern.Match(v))
|
|
||||||
}
|
|
||||||
case KindAll:
|
|
||||||
if s, ok := cur.(quantifier); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil {
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
it := res.ListIterator()
|
|
||||||
if it == nil {
|
|
||||||
return matchResultFalse, cur // not a list
|
|
||||||
}
|
|
||||||
for !it.Done() {
|
|
||||||
_, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
panic("should never happen")
|
|
||||||
}
|
|
||||||
matchRes, leaf := matchStatement(s.statement, v)
|
|
||||||
switch matchRes {
|
|
||||||
case matchResultNoData, matchResultOptionalNoData:
|
|
||||||
return matchRes, leaf
|
|
||||||
case matchResultTrue:
|
|
||||||
// continue
|
|
||||||
case matchResultFalse:
|
|
||||||
return matchResultFalse, leaf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchResultTrue, nil
|
|
||||||
}
|
|
||||||
case KindAny:
|
|
||||||
if s, ok := cur.(quantifier); ok {
|
|
||||||
res, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return matchResultNoData, cur
|
|
||||||
}
|
|
||||||
if res == nil {
|
|
||||||
return matchResultOptionalNoData, nil
|
|
||||||
}
|
|
||||||
it := res.ListIterator()
|
|
||||||
if it == nil {
|
|
||||||
return matchResultFalse, cur // not a list
|
|
||||||
}
|
|
||||||
for !it.Done() {
|
|
||||||
_, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
panic("should never happen")
|
|
||||||
}
|
|
||||||
matchRes, leaf := matchStatement(s.statement, v)
|
|
||||||
switch matchRes {
|
|
||||||
case matchResultNoData, matchResultOptionalNoData:
|
|
||||||
return matchRes, leaf
|
|
||||||
case matchResultTrue:
|
|
||||||
return matchResultTrue, nil
|
|
||||||
case matchResultFalse:
|
|
||||||
// continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchResultFalse, cur
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// isOrdered compares two IPLD nodes and returns true if they satisfy the given ordering function.
|
|
||||||
// It supports comparison of integers and floats, returning false for:
|
|
||||||
// - Nodes of different or unsupported kinds
|
|
||||||
// - Integer values outside JavaScript's safe integer bounds (±2^53-1)
|
|
||||||
// - Non-finite floating point values (NaN or ±Inf)
|
|
||||||
//
|
|
||||||
// The satisfies parameter is a function that interprets the comparison result:
|
|
||||||
// - For ">" it returns true when order is 1
|
|
||||||
// - For ">=" it returns true when order is 0 or 1
|
|
||||||
// - For "<" it returns true when order is -1
|
|
||||||
// - For "<=" it returns true when order is -1 or 0
|
|
||||||
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
|
||||||
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
|
||||||
a := must.Int(actual)
|
|
||||||
b := must.Int(expected)
|
|
||||||
|
|
||||||
return satisfies(cmp.Compare(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float {
|
|
||||||
a, err := actual.AsFloat()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("extracting node float: %w", err))
|
|
||||||
}
|
|
||||||
b, err := expected.AsFloat()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("extracting selector float: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if math.IsInf(a, 0) || math.IsNaN(a) || math.IsInf(b, 0) || math.IsNaN(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return satisfies(cmp.Compare(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func gt(order int) bool { return order == 1 }
|
|
||||||
func gte(order int) bool { return order == 0 || order == 1 }
|
|
||||||
func lt(order int) bool { return order == -1 }
|
|
||||||
func lte(order int) bool { return order == 0 || order == -1 }
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,254 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
|
|
||||||
selpkg "code.sonr.org/go/ucan/pkg/policy/selector"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
KindEqual = "==" // implemented by equality
|
|
||||||
KindNotEqual = "!=" // implemented by equality
|
|
||||||
KindGreaterThan = ">" // implemented by equality
|
|
||||||
KindGreaterThanOrEqual = ">=" // implemented by equality
|
|
||||||
KindLessThan = "<" // implemented by equality
|
|
||||||
KindLessThanOrEqual = "<=" // implemented by equality
|
|
||||||
KindNot = "not" // implemented by negation
|
|
||||||
KindAnd = "and" // implemented by connective
|
|
||||||
KindOr = "or" // implemented by connective
|
|
||||||
KindLike = "like" // implemented by wildcard
|
|
||||||
KindAll = "all" // implemented by quantifier
|
|
||||||
KindAny = "any" // implemented by quantifier
|
|
||||||
)
|
|
||||||
|
|
||||||
type Policy []Statement
|
|
||||||
|
|
||||||
type Constructor func() (Statement, error)
|
|
||||||
|
|
||||||
func Construct(cstors ...Constructor) (Policy, error) {
|
|
||||||
stmts, err := assemble(cstors)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return stmts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustConstruct(cstors ...Constructor) Policy {
|
|
||||||
pol, err := Construct(cstors...)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return pol
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Policy) String() string {
|
|
||||||
if len(p) == 0 {
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
childs := make([]string, len(p))
|
|
||||||
for i, statement := range p {
|
|
||||||
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("[\n %s\n]", strings.Join(childs, ",\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Statement interface {
|
|
||||||
Kind() string
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type equality struct {
|
|
||||||
kind string
|
|
||||||
selector selpkg.Selector
|
|
||||||
value ipld.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e equality) Kind() string {
|
|
||||||
return e.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e equality) String() string {
|
|
||||||
child, err := ipld.Encode(e.value, dagjson.Encode)
|
|
||||||
if err != nil {
|
|
||||||
return "ERROR: INVALID VALUE"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(`["%s", "%s", %s]`, e.kind, e.selector, strings.ReplaceAll(string(child), "\n", "\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Equal(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindEqual, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NotEqual(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindNotEqual, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreaterThan(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindGreaterThan, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreaterThanOrEqual(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindGreaterThanOrEqual, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LessThan(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindLessThan, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LessThanOrEqual(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindLessThanOrEqual, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type negation struct {
|
|
||||||
statement Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n negation) Kind() string {
|
|
||||||
return KindNot
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n negation) String() string {
|
|
||||||
child := n.statement.String()
|
|
||||||
return fmt.Sprintf(`["%s", %s]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Not(cstor Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmt, err := cstor()
|
|
||||||
return negation{statement: stmt}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type connective struct {
|
|
||||||
kind string
|
|
||||||
statements []Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c connective) Kind() string {
|
|
||||||
return c.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c connective) String() string {
|
|
||||||
childs := make([]string, len(c.statements))
|
|
||||||
for i, statement := range c.statements {
|
|
||||||
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("[\"%s\", [\n %s\n]]", c.kind, strings.Join(childs, ",\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func And(cstors ...Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmts, err := assemble(cstors)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return connective{kind: KindAnd, statements: stmts}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Or(cstors ...Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmts, err := assemble(cstors)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return connective{kind: KindOr, statements: stmts}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type wildcard struct {
|
|
||||||
selector selpkg.Selector
|
|
||||||
pattern glob
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n wildcard) Kind() string {
|
|
||||||
return KindLike
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n wildcard) String() string {
|
|
||||||
return fmt.Sprintf(`["%s", "%s", "%s"]`, n.Kind(), n.selector, n.pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Like(selector string, pattern string) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
g, err := parseGlob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return wildcard{selector: sel, pattern: g}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type quantifier struct {
|
|
||||||
kind string
|
|
||||||
selector selpkg.Selector
|
|
||||||
statement Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n quantifier) Kind() string {
|
|
||||||
return n.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n quantifier) String() string {
|
|
||||||
child := n.statement.String()
|
|
||||||
return fmt.Sprintf("[\"%s\", \"%s\",\n %s\n]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func All(selector string, cstor Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmt, err := cstor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return quantifier{kind: KindAll, selector: sel, statement: stmt}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Any(selector string, cstor Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmt, err := cstor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return quantifier{kind: KindAny, selector: sel, statement: stmt}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assemble(cstors []Constructor) ([]Statement, error) {
|
|
||||||
stmts := make([]Statement, 0, len(cstors))
|
|
||||||
for _, cstor := range cstors {
|
|
||||||
stmt, err := cstor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stmts = append(stmts, stmt)
|
|
||||||
}
|
|
||||||
return stmts, nil
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package policy_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/literal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExamplePolicy() {
|
|
||||||
pol := policy.MustConstruct(
|
|
||||||
policy.Equal(".status", literal.String("draft")),
|
|
||||||
policy.All(".reviewer",
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
),
|
|
||||||
policy.Any(".tags", policy.Or(
|
|
||||||
policy.Equal(".", literal.String("news")),
|
|
||||||
policy.Equal(".", literal.String("press")),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
fmt.Println(pol)
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// [
|
|
||||||
// ["==", ".status", "draft"],
|
|
||||||
// ["all", ".reviewer",
|
|
||||||
// ["like", ".email", "*@example.com"]
|
|
||||||
// ],
|
|
||||||
// ["any", ".tags",
|
|
||||||
// ["or", [
|
|
||||||
// ["==", ".", "news"],
|
|
||||||
// ["==", ".", "press"]
|
|
||||||
// ]]
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExamplePolicy_accumulate() {
|
|
||||||
var statements []policy.Constructor
|
|
||||||
|
|
||||||
statements = append(statements, policy.Equal(".status", literal.String("draft")))
|
|
||||||
|
|
||||||
statements = append(statements, policy.All(".reviewer",
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
))
|
|
||||||
|
|
||||||
statements = append(statements, policy.Any(".tags", policy.Or(
|
|
||||||
policy.Equal(".", literal.String("news")),
|
|
||||||
policy.Equal(".", literal.String("press")),
|
|
||||||
)))
|
|
||||||
|
|
||||||
pol := policy.MustConstruct(statements...)
|
|
||||||
|
|
||||||
fmt.Println(pol)
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// [
|
|
||||||
// ["==", ".status", "draft"],
|
|
||||||
// ["all", ".reviewer",
|
|
||||||
// ["like", ".email", "*@example.com"]
|
|
||||||
// ],
|
|
||||||
// ["any", ".tags",
|
|
||||||
// ["or", [
|
|
||||||
// ["==", ".", "news"],
|
|
||||||
// ["==", ".", "press"]
|
|
||||||
// ]]
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConstruct(t *testing.T) {
|
|
||||||
pol, err := policy.Construct(
|
|
||||||
policy.Equal(".status", literal.String("draft")),
|
|
||||||
policy.All(".reviewer",
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, pol)
|
|
||||||
|
|
||||||
// check if errors cascade correctly
|
|
||||||
pol, err = policy.Construct(
|
|
||||||
policy.Equal(".status", literal.String("draft")),
|
|
||||||
policy.All(".reviewer", policy.Or(
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
policy.Like(".", "\\"), // invalid pattern
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, pol)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package policytest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/args"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/literal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EmptyPolicy provides a Policy with no statements.
|
|
||||||
var EmptyPolicy = policy.Policy{}
|
|
||||||
|
|
||||||
// SpecPolicy provides a valid Policy containing the statements that are included
|
|
||||||
// in the second code block of the [Validation] section of the delegation specification.
|
|
||||||
//
|
|
||||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
|
||||||
var SpecPolicy = policy.MustConstruct(
|
|
||||||
policy.Equal(".from", literal.String("alice@example.com")),
|
|
||||||
policy.Any(".to", policy.Like(".", "*@example.com")),
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Replace the URL for [Validation] above when the delegation
|
|
||||||
// specification has been finished/merged.
|
|
||||||
|
|
||||||
// SpecValidArguments provides valid, instantiated Arguments containing
|
|
||||||
// the key/value pairs that are included in portion of the second code block
|
|
||||||
// of the [Validation] section of the delegation specification.
|
|
||||||
//
|
|
||||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
|
||||||
var SpecValidArguments = args.NewBuilder().
|
|
||||||
Add("from", "alice@example.com").
|
|
||||||
Add("to", []string{
|
|
||||||
"bob@example.com",
|
|
||||||
"carol@not.example.com",
|
|
||||||
}).
|
|
||||||
Add("title", "Coffee").
|
|
||||||
Add("body", "Still on for coffee").
|
|
||||||
MustBuild()
|
|
||||||
|
|
||||||
var specValidArgumentsIPLD = mustIPLD(SpecValidArguments)
|
|
||||||
|
|
||||||
// SpecInvalidArguments provides invalid, instantiated Arguments containing
|
|
||||||
// the key/value pairs that are included in portion of the second code block
|
|
||||||
// of the [Validation] section of the delegation specification.
|
|
||||||
//
|
|
||||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
|
||||||
var SpecInvalidArguments = args.NewBuilder().
|
|
||||||
Add("from", "alice@example.com").
|
|
||||||
Add("to", []string{
|
|
||||||
"bob@null.com",
|
|
||||||
"carol@elsewhere.example.com",
|
|
||||||
}).
|
|
||||||
Add("title", "Coffee").
|
|
||||||
Add("body", "Still on for coffee").
|
|
||||||
MustBuild()
|
|
||||||
|
|
||||||
var specInvalidArgumentsIPLD = mustIPLD(SpecInvalidArguments)
|
|
||||||
|
|
||||||
func mustIPLD(args *args.Args) ipld.Node {
|
|
||||||
node, err := args.ToIPLD()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package policytest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestInvocationValidation applies the example policy to the second
|
|
||||||
// example arguments as defined in the [Validation] section of the
|
|
||||||
// invocation specification.
|
|
||||||
//
|
|
||||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
|
||||||
func TestInvocationValidationSpecExamples(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("with passing args", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
exec, stmt := SpecPolicy.Match(specValidArgumentsIPLD)
|
|
||||||
assert.True(t, exec)
|
|
||||||
assert.Nil(t, stmt)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("fails on recipients (second statement)", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
exec, stmt := SpecPolicy.Match(specInvalidArgumentsIPLD)
|
|
||||||
assert.False(t, exec)
|
|
||||||
assert.NotNil(t, stmt)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/limits"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
|
||||||
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
|
||||||
|
|
||||||
// Field name requirements:
|
|
||||||
// - Must start with ASCII letter, Unicode letter, or underscore
|
|
||||||
// - Can contain:
|
|
||||||
// - ASCII letters (a-z, A-Z)
|
|
||||||
// - ASCII digits (0-9)
|
|
||||||
// - Unicode letters (\p{L})
|
|
||||||
// - Dollar sign ($)
|
|
||||||
// - Underscore (_)
|
|
||||||
// - Hyphen (-)
|
|
||||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_\p{L}][a-zA-Z0-9$_\p{L}\-]*$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func Parse(str string) (Selector, error) {
|
|
||||||
if len(str) == 0 {
|
|
||||||
return nil, newParseError("empty selector", str, 0, "")
|
|
||||||
}
|
|
||||||
if string(str[0]) != "." {
|
|
||||||
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
|
|
||||||
}
|
|
||||||
if str == "." {
|
|
||||||
return Selector{segment{str: ".", identity: true}}, nil
|
|
||||||
}
|
|
||||||
if str == ".?" {
|
|
||||||
return Selector{segment{str: ".?", identity: true, optional: true}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
col := 0
|
|
||||||
var sel Selector
|
|
||||||
for _, tok := range tokenize(str) {
|
|
||||||
seg := tok
|
|
||||||
opt := strings.HasSuffix(tok, "?")
|
|
||||||
if opt {
|
|
||||||
seg = strings.TrimRight(tok, "?")
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case seg == ".":
|
|
||||||
if len(sel) > 0 && sel[len(sel)-1].Identity() {
|
|
||||||
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
|
|
||||||
}
|
|
||||||
sel = append(sel, segment{str: ".", identity: true})
|
|
||||||
|
|
||||||
case seg == "[]":
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, iterator: true})
|
|
||||||
|
|
||||||
case strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]"):
|
|
||||||
lookup := seg[1 : len(seg)-1]
|
|
||||||
|
|
||||||
switch {
|
|
||||||
// index, [123]
|
|
||||||
case indexRegex.MatchString(lookup):
|
|
||||||
idx, err := strconv.Atoi(lookup)
|
|
||||||
if err != nil {
|
|
||||||
return nil, newParseError("invalid index", str, col, tok)
|
|
||||||
}
|
|
||||||
if int64(idx) > limits.MaxInt53 || int64(idx) < limits.MinInt53 {
|
|
||||||
return nil, newParseError(fmt.Sprintf("index %d exceeds safe integer bounds", idx), str, col, tok)
|
|
||||||
}
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, index: idx})
|
|
||||||
|
|
||||||
// explicit field, ["abcd"]
|
|
||||||
case strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\""):
|
|
||||||
fieldName := lookup[1 : len(lookup)-1]
|
|
||||||
if strings.Contains(fieldName, ":") {
|
|
||||||
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
|
||||||
}
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, field: fieldName})
|
|
||||||
|
|
||||||
// slice [3:5] or [:5] or [3:], also negative numbers
|
|
||||||
case sliceRegex.MatchString(lookup):
|
|
||||||
var rng [2]int64
|
|
||||||
splt := strings.Split(lookup, ":")
|
|
||||||
if splt[0] == "" {
|
|
||||||
rng[0] = math.MinInt
|
|
||||||
} else {
|
|
||||||
i, err := strconv.ParseInt(splt[0], 10, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, newParseError("invalid slice index", str, col, tok)
|
|
||||||
}
|
|
||||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
|
||||||
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
|
||||||
}
|
|
||||||
rng[0] = i
|
|
||||||
}
|
|
||||||
if splt[1] == "" {
|
|
||||||
rng[1] = math.MaxInt
|
|
||||||
} else {
|
|
||||||
i, err := strconv.ParseInt(splt[1], 10, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, newParseError("invalid slice index", str, col, tok)
|
|
||||||
}
|
|
||||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
|
||||||
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
|
||||||
}
|
|
||||||
rng[1] = i
|
|
||||||
}
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, slice: rng[:]})
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
case fieldRegex.MatchString(seg):
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]})
|
|
||||||
default:
|
|
||||||
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
|
||||||
}
|
|
||||||
col += len(tok)
|
|
||||||
}
|
|
||||||
return sel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustParse(sel string) Selector {
|
|
||||||
s, err := Parse(sel)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenize(str string) []string {
|
|
||||||
var toks []string
|
|
||||||
col := 0
|
|
||||||
ofs := 0
|
|
||||||
ctx := ""
|
|
||||||
|
|
||||||
for col < len(str) {
|
|
||||||
char := string(str[col])
|
|
||||||
|
|
||||||
if char == "\"" && string(str[col-1]) != "\\" {
|
|
||||||
col++
|
|
||||||
if ctx == "\"" {
|
|
||||||
ctx = ""
|
|
||||||
} else {
|
|
||||||
ctx = "\""
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx == "\"" {
|
|
||||||
col++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if char == "." || char == "[" {
|
|
||||||
if ofs < col {
|
|
||||||
toks = append(toks, str[ofs:col])
|
|
||||||
}
|
|
||||||
ofs = col
|
|
||||||
}
|
|
||||||
col++
|
|
||||||
}
|
|
||||||
|
|
||||||
if ofs < col && ctx != "\"" {
|
|
||||||
toks = append(toks, str[ofs:col])
|
|
||||||
}
|
|
||||||
|
|
||||||
return toks
|
|
||||||
}
|
|
||||||
|
|
||||||
type parseErr struct {
|
|
||||||
msg string
|
|
||||||
src string
|
|
||||||
col int
|
|
||||||
tok string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseErr) Name() string {
|
|
||||||
return "ParseError"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseErr) Message() string {
|
|
||||||
return p.msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseErr) Column() int {
|
|
||||||
return p.col
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseErr) Error() string {
|
|
||||||
return p.msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseErr) Source() string {
|
|
||||||
return p.src
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseErr) Token() string {
|
|
||||||
return p.tok
|
|
||||||
}
|
|
||||||
|
|
||||||
func newParseError(message string, source string, column int, token string) error {
|
|
||||||
return parseErr{message, source, column, token}
|
|
||||||
}
|
|
||||||
@@ -1,641 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy/limits"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
|
||||||
t.Run("identity", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("dotted field name", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".foo")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".foo_bar")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo_bar")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".foo-bar")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo-bar")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".foo*bar")
|
|
||||||
require.ErrorContains(t, err, "invalid segment")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("explicit field", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.["foo"]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Equal(t, sel[1].Field(), "foo")
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("iterator, collection value", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.True(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("index", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[138]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Equal(t, sel[1].Index(), 138)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("negative index", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[-138]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Equal(t, sel[1].Index(), -138)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("List slice", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[7:11]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{7, 11})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".[2:]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{2, math.MaxInt})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".[:42]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{math.MinInt, 42})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".[0:-2]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{0, -2})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional identity", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.True(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional dotted field name", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".foo?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.True(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional explicit field", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.["foo"]?`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Equal(t, sel[1].Field(), "foo")
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional iterator", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.True(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional index", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[138]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Equal(t, sel[1].Index(), 138)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional negative index", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[-138]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Equal(t, sel[1].Index(), -138)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional list slice", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[7:11]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{7, 11})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".[2:]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{2, math.MaxInt})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".[:42]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{math.MinInt, 42})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
|
|
||||||
sel, err = Parse(".[0:-2]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{0, -2})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("idempotent optional", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".foo???")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.True(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("deny multi dot", func(t *testing.T) {
|
|
||||||
_, err := Parse("..")
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("nesting", func(t *testing.T) {
|
|
||||||
str := `.foo.["bar"].[138]?.baz[1:]`
|
|
||||||
sel, err := Parse(str)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, str, sel.String())
|
|
||||||
require.Equal(t, 7, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.True(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
require.False(t, sel[2].Identity())
|
|
||||||
require.False(t, sel[2].Optional())
|
|
||||||
require.False(t, sel[2].Iterator())
|
|
||||||
require.Empty(t, sel[2].Slice())
|
|
||||||
require.Equal(t, sel[2].Field(), "bar")
|
|
||||||
require.Empty(t, sel[2].Index())
|
|
||||||
require.True(t, sel[3].Identity())
|
|
||||||
require.False(t, sel[3].Optional())
|
|
||||||
require.False(t, sel[3].Iterator())
|
|
||||||
require.Empty(t, sel[3].Slice())
|
|
||||||
require.Empty(t, sel[3].Field())
|
|
||||||
require.Empty(t, sel[3].Index())
|
|
||||||
require.False(t, sel[4].Identity())
|
|
||||||
require.True(t, sel[4].Optional())
|
|
||||||
require.False(t, sel[4].Iterator())
|
|
||||||
require.Empty(t, sel[4].Slice())
|
|
||||||
require.Empty(t, sel[4].Field())
|
|
||||||
require.Equal(t, sel[4].Index(), 138)
|
|
||||||
require.False(t, sel[5].Identity())
|
|
||||||
require.False(t, sel[5].Optional())
|
|
||||||
require.False(t, sel[5].Iterator())
|
|
||||||
require.Empty(t, sel[5].Slice())
|
|
||||||
require.Equal(t, sel[5].Field(), "baz")
|
|
||||||
require.Empty(t, sel[5].Index())
|
|
||||||
require.False(t, sel[6].Identity())
|
|
||||||
require.False(t, sel[6].Optional())
|
|
||||||
require.False(t, sel[6].Iterator())
|
|
||||||
require.Equal(t, sel[6].Slice(), []int64{1, math.MaxInt})
|
|
||||||
require.Empty(t, sel[6].Field())
|
|
||||||
require.Empty(t, sel[6].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non dotted", func(t *testing.T) {
|
|
||||||
_, err := Parse("foo")
|
|
||||||
require.NotNil(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non quoted", func(t *testing.T) {
|
|
||||||
_, err := Parse(".[foo]")
|
|
||||||
require.NotNil(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice with negative start and positive end", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[0:-2]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{0, -2})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice with start greater than end", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[5:2]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{5, 2})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice on string", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.["foo"].[1:3]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 4, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Equal(t, sel[1].Field(), "foo")
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
require.True(t, sel[2].Identity())
|
|
||||||
require.False(t, sel[2].Optional())
|
|
||||||
require.False(t, sel[2].Iterator())
|
|
||||||
require.Empty(t, sel[2].Slice())
|
|
||||||
require.Empty(t, sel[2].Field())
|
|
||||||
require.Empty(t, sel[2].Index())
|
|
||||||
require.False(t, sel[3].Identity())
|
|
||||||
require.False(t, sel[3].Optional())
|
|
||||||
require.False(t, sel[3].Iterator())
|
|
||||||
require.Equal(t, sel[3].Slice(), []int64{1, 3})
|
|
||||||
require.Empty(t, sel[3].Field())
|
|
||||||
require.Empty(t, sel[3].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice on array", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.[1:3]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Equal(t, sel[1].Slice(), []int64{1, 3})
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("index on array", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.[1]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Equal(t, sel[1].Index(), 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid slice on object", func(t *testing.T) {
|
|
||||||
_, err := Parse(`.["foo":"bar"]`)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "invalid segment")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("index on object", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.["foo"]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Equal(t, sel[1].Field(), "foo")
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice with non-integer start", func(t *testing.T) {
|
|
||||||
_, err := Parse(".[foo:3]")
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice with non-integer end", func(t *testing.T) {
|
|
||||||
_, err := Parse(".[1:bar]")
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("index with non-integer", func(t *testing.T) {
|
|
||||||
_, err := Parse(".[foo]")
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("extended field names", func(t *testing.T) {
|
|
||||||
validFields := []string{
|
|
||||||
".basic",
|
|
||||||
".user_name",
|
|
||||||
".user-name",
|
|
||||||
".userName$special",
|
|
||||||
".αβγ", // Greek letters
|
|
||||||
".użytkownik", // Polish characters
|
|
||||||
".用户", // Chinese characters
|
|
||||||
".사용자", // Korean characters
|
|
||||||
"._private",
|
|
||||||
".number123",
|
|
||||||
".camelCase",
|
|
||||||
".snake_case",
|
|
||||||
".kebab-case",
|
|
||||||
".mixed_kebab-case",
|
|
||||||
".with$dollar",
|
|
||||||
".MIXED_Case_123",
|
|
||||||
".unicodeø",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, field := range validFields {
|
|
||||||
sel, err := Parse(field)
|
|
||||||
require.NoError(t, err, "field: %s", field)
|
|
||||||
require.NotNil(t, sel)
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidFields := []string{
|
|
||||||
".123number", // Can't start with digit
|
|
||||||
".@special", // @ not allowed
|
|
||||||
".space name", // No spaces
|
|
||||||
".#hashtag", // No #
|
|
||||||
".name!", // No !
|
|
||||||
".{brackets}", // No brackets
|
|
||||||
".name/with/slashes", // No slashes
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, field := range invalidFields {
|
|
||||||
sel, err := Parse(field)
|
|
||||||
require.Error(t, err, "field: %s", field)
|
|
||||||
require.Nil(t, sel)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("integer overflow", func(t *testing.T) {
|
|
||||||
sel, err := Parse(fmt.Sprintf(".[%d]", limits.MaxInt53+1))
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, sel)
|
|
||||||
|
|
||||||
sel, err = Parse(fmt.Sprintf(".[%d]", limits.MinInt53-1))
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, sel)
|
|
||||||
|
|
||||||
// Test slice overflow
|
|
||||||
sel, err = Parse(fmt.Sprintf(".[%d:42]", limits.MaxInt53+1))
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, sel)
|
|
||||||
|
|
||||||
sel, err = Parse(fmt.Sprintf(".[1:%d]", limits.MaxInt53+1))
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, sel)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Selector describes a UCAN policy selector, as specified here:
|
|
||||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
|
|
||||||
type Selector []segment
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
return resolve(s, subject, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Selector) String() string {
|
|
||||||
var res strings.Builder
|
|
||||||
for _, seg := range s {
|
|
||||||
res.WriteString(seg.String())
|
|
||||||
}
|
|
||||||
return res.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type segment struct {
|
|
||||||
str string
|
|
||||||
identity bool
|
|
||||||
optional bool
|
|
||||||
iterator bool
|
|
||||||
slice []int64 // either 0-length or 2-length
|
|
||||||
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() []int64 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, error) {
|
|
||||||
errIfNotOptional := func(s segment, err error) error {
|
|
||||||
if !s.Optional() {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cur := subject
|
|
||||||
for _, seg := range sel {
|
|
||||||
// 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
|
|
||||||
|
|
||||||
case seg.Iterator():
|
|
||||||
switch {
|
|
||||||
case cur == nil || cur.Kind() == datamodel.Kind_Null:
|
|
||||||
if seg.Optional() {
|
|
||||||
// build an empty list
|
|
||||||
n, _ := qp.BuildList(basicnode.Prototype.Any, 0, func(_ datamodel.ListAssembler) {})
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
return nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
it := cur.MapIterator()
|
|
||||||
for !it.Done() {
|
|
||||||
_, v, err := it.Next()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
qp.ListEntry(l, qp.Node(v))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic("should never happen")
|
|
||||||
}
|
|
||||||
return nd, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
|
||||||
}
|
|
||||||
|
|
||||||
case seg.Field() != "":
|
|
||||||
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
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cur = n
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
err := newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
|
||||||
return nil, errIfNotOptional(seg, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case len(seg.Slice()) > 0:
|
|
||||||
if cur == nil {
|
|
||||||
err := newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
|
|
||||||
return nil, errIfNotOptional(seg, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
qp.ListEntry(l, qp.Node(item))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic("should never happen")
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
default: // Index()
|
|
||||||
at = append(at, strconv.Itoa(seg.Index()))
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// segment exhausted, we return where we are
|
|
||||||
return cur, 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 **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")
|
|
||||||
}
|
|
||||||
|
|
||||||
start, end = slice[0], slice[1]
|
|
||||||
|
|
||||||
// adjust boundaries
|
|
||||||
switch {
|
|
||||||
case slice[0] == math.MinInt:
|
|
||||||
start = 0
|
|
||||||
case slice[0] < 0:
|
|
||||||
// Check for potential overflow before adding
|
|
||||||
if -slice[0] > length {
|
|
||||||
start = 0
|
|
||||||
} else {
|
|
||||||
start = length + slice[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case slice[1] == math.MaxInt:
|
|
||||||
end = length
|
|
||||||
case slice[1] < 0:
|
|
||||||
// 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
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// clamp out of bound
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if start > length {
|
|
||||||
start = length
|
|
||||||
}
|
|
||||||
if end < 0 {
|
|
||||||
end = 0
|
|
||||||
}
|
|
||||||
if end > length {
|
|
||||||
end = length
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return resolutionErr{message, at}
|
|
||||||
}
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
"github.com/ipld/go-ipld-prime/must"
|
|
||||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSelect(t *testing.T) {
|
|
||||||
type name struct {
|
|
||||||
First string
|
|
||||||
Middle *string
|
|
||||||
Last string
|
|
||||||
}
|
|
||||||
type interest struct {
|
|
||||||
Name string
|
|
||||||
Outdoor bool
|
|
||||||
Experience int
|
|
||||||
}
|
|
||||||
type user struct {
|
|
||||||
Name name
|
|
||||||
Age int
|
|
||||||
Nationalities []string
|
|
||||||
Interests []interest
|
|
||||||
}
|
|
||||||
|
|
||||||
ts, err := ipld.LoadSchemaBytes([]byte(`
|
|
||||||
type User struct {
|
|
||||||
name Name
|
|
||||||
age Int
|
|
||||||
nationalities [String]
|
|
||||||
interests [Interest]
|
|
||||||
}
|
|
||||||
type Name struct {
|
|
||||||
first String
|
|
||||||
middle optional String
|
|
||||||
last String
|
|
||||||
}
|
|
||||||
type Interest struct {
|
|
||||||
name String
|
|
||||||
outdoor Bool
|
|
||||||
experience Int
|
|
||||||
}
|
|
||||||
`))
|
|
||||||
require.NoError(t, err)
|
|
||||||
typ := ts.TypeByName("User")
|
|
||||||
|
|
||||||
am := "Joan"
|
|
||||||
alice := user{
|
|
||||||
Name: name{First: "Alice", Middle: &am, Last: "Wonderland"},
|
|
||||||
Age: 24,
|
|
||||||
Nationalities: []string{"British"},
|
|
||||||
Interests: []interest{
|
|
||||||
{Name: "Cycling", Outdoor: true, Experience: 4},
|
|
||||||
{Name: "Chess", Outdoor: false, Experience: 2},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bob := user{
|
|
||||||
Name: name{First: "Bob", Last: "Builder"},
|
|
||||||
Age: 35,
|
|
||||||
Nationalities: []string{"Canadian", "South African"},
|
|
||||||
Interests: []interest{
|
|
||||||
{Name: "Snowboarding", Outdoor: true, Experience: 8},
|
|
||||||
{Name: "Reading", Outdoor: false, Experience: 25},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
anode := bindnode.Wrap(&alice, typ)
|
|
||||||
bnode := bindnode.Wrap(&bob, typ)
|
|
||||||
|
|
||||||
t.Run("identity", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := sel.Select(anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
age := must.Int(must.Node(res.LookupByString("age")))
|
|
||||||
require.Equal(t, int64(alice.Age), age)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("nested property", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".name.first")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := sel.Select(anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
name := must.String(res)
|
|
||||||
require.Equal(t, alice.Name.First, name)
|
|
||||||
|
|
||||||
res, err = sel.Select(bnode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
name = must.String(res)
|
|
||||||
require.Equal(t, bob.Name.First, name)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional nested property", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".name.middle?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := sel.Select(anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
name := must.String(res)
|
|
||||||
require.Equal(t, *alice.Name.Middle, name)
|
|
||||||
|
|
||||||
res, err = sel.Select(bnode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, res)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("not exists", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".name.foo")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := sel.Select(anode)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Empty(t, res)
|
|
||||||
|
|
||||||
require.ErrorAs(t, err, &resolutionErr{}, "error should be a resolution error")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional not exists", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".name.foo?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
one, err := sel.Select(anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, one)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("iterator", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".interests[]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := sel.Select(anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
iname := must.String(must.Node(must.Node(res.LookupByIndex(0)).LookupByString("name")))
|
|
||||||
require.Equal(t, alice.Interests[0].Name, iname)
|
|
||||||
|
|
||||||
iname = must.String(must.Node(must.Node(res.LookupByIndex(1)).LookupByString("name")))
|
|
||||||
require.Equal(t, alice.Interests[1].Name, iname)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice on string", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.[1:3]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
node := basicnode.NewString("hello")
|
|
||||||
res, err := sel.Select(node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
str, err := res.AsString()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "el", str) // assert sliced substring
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("out of bounds slicing", func(t *testing.T) {
|
|
||||||
node, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) {
|
|
||||||
qp.ListEntry(la, qp.Int(1))
|
|
||||||
qp.ListEntry(la, qp.Int(2))
|
|
||||||
qp.ListEntry(la, qp.Int(3))
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
sel, err := Parse(`.[10:20]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := sel.Select(node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
require.Equal(t, int64(0), res.Length())
|
|
||||||
|
|
||||||
_, err = res.LookupByIndex(0)
|
|
||||||
require.ErrorIs(t, err, datamodel.ErrNotExists{}) // assert empty result for out of bounds slice
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("backward slicing", func(t *testing.T) {
|
|
||||||
node, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) {
|
|
||||||
qp.ListEntry(la, qp.Int(1))
|
|
||||||
qp.ListEntry(la, qp.Int(2))
|
|
||||||
qp.ListEntry(la, qp.Int(3))
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
sel, err := Parse(`.[5:2]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := sel.Select(node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
require.Equal(t, int64(0), res.Length())
|
|
||||||
|
|
||||||
_, err = res.LookupByIndex(0)
|
|
||||||
require.ErrorIs(t, err, datamodel.ErrNotExists{}) // assert empty result for backward slice
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice with negative index", func(t *testing.T) {
|
|
||||||
node, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) {
|
|
||||||
qp.ListEntry(la, qp.Int(1))
|
|
||||||
qp.ListEntry(la, qp.Int(2))
|
|
||||||
qp.ListEntry(la, qp.Int(3))
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
sel, err := Parse(`.[0:-1]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := sel.Select(node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
val, err := res.LookupByIndex(1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, int(must.Int(val))) // Assert sliced value at index 1
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("slice on bytes", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.[1:3]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
node := basicnode.NewBytes([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
|
|
||||||
res, err := sel.Select(node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
bytes, err := res.AsBytes()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, []byte{0x02, 0x03}, bytes) // assert sliced bytes
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("index on bytes", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.[2]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
node := basicnode.NewBytes([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
|
|
||||||
res, err := sel.Select(node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
|
|
||||||
val, err := res.AsInt()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, int64(0x03), val) // assert indexed byte value
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("out of bounds slicing on bytes", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.[10:20]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
node := basicnode.NewBytes([]byte{0x01, 0x02, 0x03})
|
|
||||||
res, err := sel.Select(node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, res)
|
|
||||||
|
|
||||||
bytes, err := res.AsBytes()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, bytes) // assert empty result for out of bounds slice
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("out of bounds indexing on bytes", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.[10]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
node := basicnode.NewBytes([]byte{0x01, 0x02, 0x03})
|
|
||||||
_, err = sel.Select(node)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "can not resolve path: .10") // assert error for out of bounds index
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzParse(f *testing.F) {
|
|
||||||
selectorCorpus := []string{
|
|
||||||
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
|
||||||
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
|
|
||||||
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
|
|
||||||
`.length`, `.[4]`,
|
|
||||||
}
|
|
||||||
for _, selector := range selectorCorpus {
|
|
||||||
f.Add(selector)
|
|
||||||
}
|
|
||||||
f.Fuzz(func(t *testing.T, selector string) {
|
|
||||||
// only look for panic()
|
|
||||||
_, _ = Parse(selector)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzParseAndSelect(f *testing.F) {
|
|
||||||
selectorCorpus := []string{
|
|
||||||
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
|
||||||
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
|
|
||||||
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
|
|
||||||
`.length`, `.[4]`,
|
|
||||||
}
|
|
||||||
subjectCorpus := []string{
|
|
||||||
`{"x":1}`, `[1, 2]`, `null`, `[[1], 2, [3]]`, `{"x": 1 }`, `{"x": 1}`,
|
|
||||||
`[1, 2]`, `[1, 2]`, `"Hi"`, `{"/":{"bytes":"AAE"}`, `[0, 1, 2]`,
|
|
||||||
`[0, 1, 2]`, `[0, 1, 2]`, `"hello"`, `{"/":{"bytes":"AAEC"}}`, `{}`,
|
|
||||||
`null`, `[]`, `{}`, `[1, 2]`, `[0, 1]`, `null`, `[[1], 2, [3]]`, `{}`,
|
|
||||||
`null`, `[]`, `[1, 2]`, `[0, 1]`,
|
|
||||||
}
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
switch {
|
|
||||||
case i < len(selectorCorpus) && i < len(subjectCorpus):
|
|
||||||
f.Add(selectorCorpus[i], subjectCorpus[i])
|
|
||||||
continue
|
|
||||||
case i > len(selectorCorpus):
|
|
||||||
f.Add("", subjectCorpus[i])
|
|
||||||
continue
|
|
||||||
case i > len(subjectCorpus):
|
|
||||||
f.Add(selectorCorpus[i], "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, selector, subject string) {
|
|
||||||
sel, err := Parse(selector)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
np := basicnode.Prototype.Any
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
err = dagjson.Decode(nb, strings.NewReader(subject))
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
node := nb.Build()
|
|
||||||
if node == nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for panic()
|
|
||||||
_, err = sel.Select(node)
|
|
||||||
if err != nil && !errors.As(err, &resolutionErr{}) {
|
|
||||||
// not normal, we should only have resolution errors
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveSliceIndices(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
slice []int64
|
|
||||||
length int64
|
|
||||||
wantStart int64
|
|
||||||
wantEnd int64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "normal case",
|
|
||||||
slice: []int64{1, 3},
|
|
||||||
length: 5,
|
|
||||||
wantStart: 1,
|
|
||||||
wantEnd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative indices",
|
|
||||||
slice: []int64{-2, -1},
|
|
||||||
length: 5,
|
|
||||||
wantStart: 3,
|
|
||||||
wantEnd: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "overflow protection negative start",
|
|
||||||
slice: []int64{math.MinInt64, 3},
|
|
||||||
length: 5,
|
|
||||||
wantStart: 0,
|
|
||||||
wantEnd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "overflow protection negative end",
|
|
||||||
slice: []int64{0, math.MinInt64},
|
|
||||||
length: 5,
|
|
||||||
wantStart: 0,
|
|
||||||
wantEnd: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "max bounds",
|
|
||||||
slice: []int64{0, math.MaxInt64},
|
|
||||||
length: 5,
|
|
||||||
wantStart: 0,
|
|
||||||
wantEnd: 5,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
start, end := resolveSliceIndices(tt.slice, tt.length)
|
|
||||||
require.Equal(t, tt.wantStart, start)
|
|
||||||
require.Equal(t, tt.wantEnd, end)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package secretbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/nacl/secretbox"
|
|
||||||
)
|
|
||||||
|
|
||||||
const keySize = 32 // secretbox allows only 32-byte keys
|
|
||||||
|
|
||||||
var ErrShortCipherText = errors.New("ciphertext too short")
|
|
||||||
var ErrNoEncryptionKey = errors.New("encryption key is required")
|
|
||||||
var ErrInvalidKeySize = errors.New("invalid key size: must be 32 bytes")
|
|
||||||
var ErrZeroKey = errors.New("encryption key cannot be all zeros")
|
|
||||||
|
|
||||||
// GenerateKey generates a random 32-byte key to be used by EncryptWithKey and DecryptWithKey
|
|
||||||
func GenerateKey() ([]byte, error) {
|
|
||||||
key := make([]byte, keySize)
|
|
||||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to generate key: %w", err)
|
|
||||||
}
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncryptWithKey encrypts data using NaCl's secretbox with the provided key.
|
|
||||||
// 40 bytes of overhead (24-byte nonce + 16-byte MAC) are added to the plaintext size.
|
|
||||||
func EncryptWithKey(data, key []byte) ([]byte, error) {
|
|
||||||
if err := validateKey(key); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var secretKey [keySize]byte
|
|
||||||
copy(secretKey[:], key)
|
|
||||||
|
|
||||||
// Generate 24 bytes of random data as nonce
|
|
||||||
var nonce [24]byte
|
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt and authenticate data
|
|
||||||
encrypted := secretbox.Seal(nonce[:], data, &nonce, &secretKey)
|
|
||||||
return encrypted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecryptStringWithKey decrypts data using secretbox with the provided key
|
|
||||||
func DecryptStringWithKey(data, key []byte) ([]byte, error) {
|
|
||||||
if err := validateKey(key); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) < 24 {
|
|
||||||
return nil, ErrShortCipherText
|
|
||||||
}
|
|
||||||
|
|
||||||
var secretKey [keySize]byte
|
|
||||||
copy(secretKey[:], key)
|
|
||||||
|
|
||||||
var nonce [24]byte
|
|
||||||
copy(nonce[:], data[:24])
|
|
||||||
|
|
||||||
decrypted, ok := secretbox.Open(nil, data[24:], &nonce, &secretKey)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("decryption failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return decrypted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateKey(key []byte) error {
|
|
||||||
if key == nil {
|
|
||||||
return ErrNoEncryptionKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(key) != keySize {
|
|
||||||
return ErrInvalidKeySize
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if key is all zeros
|
|
||||||
for _, b := range key {
|
|
||||||
if b != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrZeroKey
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
package secretbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSecretBoxEncryption(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
key := make([]byte, keySize) // generate random 32-byte key
|
|
||||||
_, errKey := rand.Read(key)
|
|
||||||
require.NoError(t, errKey)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data []byte
|
|
||||||
key []byte
|
|
||||||
wantErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid encryption/decryption",
|
|
||||||
data: []byte("hello world"),
|
|
||||||
key: key,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil key returns error",
|
|
||||||
data: []byte("hello world"),
|
|
||||||
key: nil,
|
|
||||||
wantErr: ErrNoEncryptionKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty data",
|
|
||||||
data: []byte{},
|
|
||||||
key: key,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid key size",
|
|
||||||
data: []byte("hello world"),
|
|
||||||
key: make([]byte, 16), // Only 32 bytes allowed now
|
|
||||||
wantErr: ErrInvalidKeySize,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zero key returns error",
|
|
||||||
data: []byte("hello world"),
|
|
||||||
key: make([]byte, keySize),
|
|
||||||
wantErr: ErrZeroKey,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
encrypted, err := EncryptWithKey(tt.data, tt.key)
|
|
||||||
if tt.wantErr != nil {
|
|
||||||
require.ErrorIs(t, err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify encrypted data is different and includes nonce
|
|
||||||
require.Greater(t, len(encrypted), 24) // At least nonce size
|
|
||||||
if len(tt.data) > 0 {
|
|
||||||
require.NotEqual(t, tt.data, encrypted[24:]) // Ignore nonce prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypted, err := DecryptStringWithKey(encrypted, tt.key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, bytes.Equal(tt.data, decrypted))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecryptionErrors(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
key := make([]byte, keySize)
|
|
||||||
_, err := rand.Read(key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Create valid encrypted data for tampering tests
|
|
||||||
validData := []byte("test message")
|
|
||||||
encrypted, err := EncryptWithKey(validData, key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data []byte
|
|
||||||
key []byte
|
|
||||||
errMsg string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "short ciphertext",
|
|
||||||
data: make([]byte, 23), // Less than nonce size
|
|
||||||
key: key,
|
|
||||||
errMsg: "ciphertext too short",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid ciphertext",
|
|
||||||
data: make([]byte, 24), // Just nonce size
|
|
||||||
key: key,
|
|
||||||
errMsg: "decryption failed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tampered ciphertext",
|
|
||||||
data: tamperWithBytes(encrypted),
|
|
||||||
key: key,
|
|
||||||
errMsg: "decryption failed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing key",
|
|
||||||
data: encrypted,
|
|
||||||
key: nil,
|
|
||||||
errMsg: "encryption key is required",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, err := DecryptStringWithKey(tt.data, tt.key)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), tt.errMsg)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tamperWithBytes modifies a byte in the encrypted data to simulate tampering
|
|
||||||
func tamperWithBytes(data []byte) []byte {
|
|
||||||
if len(data) < 25 { // Need at least nonce + 1 byte
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
tampered := make([]byte, len(data))
|
|
||||||
copy(tampered, data)
|
|
||||||
tampered[24] ^= 0x01 // Modify first byte after nonce
|
|
||||||
return tampered
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
// Package delegation implements the UCAN [delegation] specification with
|
|
||||||
// an immutable Token type as well as methods to convert the Token to and
|
|
||||||
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
|
|
||||||
// should most commonly be used for transport and storage.
|
|
||||||
//
|
|
||||||
// [delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld
|
|
||||||
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
package delegation
|
|
||||||
|
|
||||||
// TODO: change the "delegation" link above when the specification is merged
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.sonr.org/go/did-it"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/command"
|
|
||||||
"code.sonr.org/go/ucan/pkg/meta"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy"
|
|
||||||
"code.sonr.org/go/ucan/token/internal/nonce"
|
|
||||||
"code.sonr.org/go/ucan/token/internal/parse"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token is an immutable type that holds the fields of a UCAN delegation.
|
|
||||||
type Token struct {
|
|
||||||
// Issuer DID (sender)
|
|
||||||
issuer did.DID
|
|
||||||
// Audience DID (receiver)
|
|
||||||
audience did.DID
|
|
||||||
// Principal that the chain is about (the Subject)
|
|
||||||
subject did.DID
|
|
||||||
// The Command to eventually invoke
|
|
||||||
command command.Command
|
|
||||||
// The delegation policy
|
|
||||||
policy policy.Policy
|
|
||||||
// A unique, random nonce
|
|
||||||
nonce []byte
|
|
||||||
// Arbitrary Metadata
|
|
||||||
meta *meta.Meta
|
|
||||||
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
|
||||||
notBefore *time.Time
|
|
||||||
// The timestamp at which the Invocation becomes invalid
|
|
||||||
expiration *time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a validated delegation Token from the provided parameters and options.
|
|
||||||
// This is typically used to delegate a given power to another agent.
|
|
||||||
//
|
|
||||||
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
|
|
||||||
func New(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, sub did.DID, opts ...Option) (*Token, error) {
|
|
||||||
tkn := &Token{
|
|
||||||
issuer: iss,
|
|
||||||
audience: aud,
|
|
||||||
subject: sub,
|
|
||||||
command: cmd,
|
|
||||||
policy: pol,
|
|
||||||
meta: meta.NewMeta(),
|
|
||||||
nonce: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
if err := opt(tkn); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if len(tkn.nonce) == 0 {
|
|
||||||
tkn.nonce, err = nonce.Generate()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tkn.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root creates a validated UCAN delegation Token from the provided parameters and options.
|
|
||||||
// This is typically used to create and give power to an agent.
|
|
||||||
//
|
|
||||||
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on itself".
|
|
||||||
func Root(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
|
||||||
return New(iss, aud, cmd, pol, iss, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Powerline creates a validated UCAN delegation Token from the provided parameters and options.
|
|
||||||
//
|
|
||||||
// Powerline is a pattern for automatically delegating all future delegations to another agent regardless of Subject.
|
|
||||||
// This is a very powerful pattern, use it only if you understand it.
|
|
||||||
// Powerline delegations MUST NOT be used as the root delegation to a resource
|
|
||||||
//
|
|
||||||
// A very common use case for Powerline is providing a stable DID across multiple agents (e.g. representing a user with
|
|
||||||
// multiple devices). This enables the automatic sharing of authority across their devices without needing to share keys
|
|
||||||
// or set up a threshold scheme. It is also flexible, since a Powerline delegation MAY be revoked.
|
|
||||||
//
|
|
||||||
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on anything".
|
|
||||||
func Powerline(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
|
||||||
return New(iss, aud, cmd, pol, nil, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issuer returns the did.DID representing the Token's issuer.
|
|
||||||
func (t *Token) Issuer() did.DID {
|
|
||||||
return t.issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audience returns the did.DID representing the Token's audience.
|
|
||||||
func (t *Token) Audience() did.DID {
|
|
||||||
return t.audience
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subject returns the did.DID representing the Token's subject.
|
|
||||||
//
|
|
||||||
// This field may be did.Undef for delegations that are [Powerlined] but
|
|
||||||
// must be equal to the value returned by the Issuer method for root
|
|
||||||
// tokens.
|
|
||||||
func (t *Token) Subject() did.DID {
|
|
||||||
return t.subject
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command returns the capability's command.Command.
|
|
||||||
func (t *Token) Command() command.Command {
|
|
||||||
return t.command
|
|
||||||
}
|
|
||||||
|
|
||||||
// Policy returns the capability's policy.Policy.
|
|
||||||
func (t *Token) Policy() policy.Policy {
|
|
||||||
return t.policy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nonce returns the random Nonce encapsulated in this Token.
|
|
||||||
func (t *Token) Nonce() []byte {
|
|
||||||
return t.nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meta returns the Token's metadata.
|
|
||||||
func (t *Token) Meta() meta.ReadOnly {
|
|
||||||
return t.meta.ReadOnly()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotBefore returns the time at which the Token becomes "active".
|
|
||||||
func (t *Token) NotBefore() *time.Time {
|
|
||||||
return t.notBefore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expiration returns the time at which the Token expires.
|
|
||||||
func (t *Token) Expiration() *time.Time {
|
|
||||||
return t.expiration
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRoot tells if the token is a root delegation.
|
|
||||||
func (t *Token) IsRoot() bool {
|
|
||||||
return t.issuer.Equal(t.subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsPowerline tells if the token is a powerline delegation.
|
|
||||||
func (t *Token) IsPowerline() bool {
|
|
||||||
return t.subject == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
|
|
||||||
// This does NOT do any other kind of verifications.
|
|
||||||
func (t *Token) IsValidNow() bool {
|
|
||||||
return t.IsValidAt(time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields.
|
|
||||||
// This does NOT do any other kind of verifications.
|
|
||||||
func (t *Token) IsValidAt(ti time.Time) bool {
|
|
||||||
if t.expiration != nil && ti.After(*t.expiration) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if t.notBefore != nil && ti.Before(*t.notBefore) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) String() string {
|
|
||||||
var res strings.Builder
|
|
||||||
|
|
||||||
var kind string
|
|
||||||
switch {
|
|
||||||
case t.issuer == t.subject:
|
|
||||||
kind = " (root delegation)"
|
|
||||||
case t.subject == nil:
|
|
||||||
kind = " (powerline delegation)"
|
|
||||||
default:
|
|
||||||
kind = " (normal delegation)"
|
|
||||||
}
|
|
||||||
|
|
||||||
res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
|
|
||||||
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
|
|
||||||
res.WriteString(fmt.Sprintf("Subject: %s%s\n", t.Subject(), kind))
|
|
||||||
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
|
|
||||||
res.WriteString(fmt.Sprintf("Policy: %s\n", t.Policy()))
|
|
||||||
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
|
|
||||||
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
|
|
||||||
res.WriteString(fmt.Sprintf("NotBefore: %v\n", t.NotBefore()))
|
|
||||||
res.WriteString(fmt.Sprintf("Expiration: %v", t.Expiration()))
|
|
||||||
|
|
||||||
return res.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) validate() error {
|
|
||||||
var errs error
|
|
||||||
|
|
||||||
requiredDID := func(id did.DID, fieldname string) {
|
|
||||||
if id == nil {
|
|
||||||
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requiredDID(t.issuer, "Issuer")
|
|
||||||
requiredDID(t.audience, "Audience")
|
|
||||||
|
|
||||||
if len(t.nonce) < 12 {
|
|
||||||
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenFromModel build a decoded view of the raw IPLD data.
|
|
||||||
// This function also serves as validation.
|
|
||||||
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|
||||||
var (
|
|
||||||
tkn Token
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
tkn.issuer, err = did.Parse(m.Iss)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse iss: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tkn.audience, err = did.Parse(m.Aud); err != nil {
|
|
||||||
return nil, fmt.Errorf("parse audience: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tkn.subject, err = parse.OptionalDID(m.Sub); err != nil {
|
|
||||||
return nil, fmt.Errorf("parse subject: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tkn.command, err = command.Parse(m.Cmd); err != nil {
|
|
||||||
return nil, fmt.Errorf("parse command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tkn.policy, err = policy.FromIPLD(m.Pol); err != nil {
|
|
||||||
return nil, fmt.Errorf("parse policy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.Nonce) == 0 {
|
|
||||||
return nil, fmt.Errorf("nonce is required")
|
|
||||||
}
|
|
||||||
tkn.nonce = m.Nonce
|
|
||||||
|
|
||||||
tkn.meta = m.Meta
|
|
||||||
if tkn.meta == nil {
|
|
||||||
tkn.meta = meta.NewMeta()
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn.notBefore, err = parse.OptionalTimestamp(m.Nbf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse notBefore: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn.expiration, err = parse.OptionalTimestamp(m.Exp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse expiration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tkn.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &tkn, nil
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package delegation_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"encoding/base64"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.sonr.org/go/did-it/didtest"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"code.sonr.org/go/ucan/pkg/command"
|
|
||||||
"code.sonr.org/go/ucan/pkg/policy"
|
|
||||||
"code.sonr.org/go/ucan/token/delegation"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed testdata/new.dagjson
|
|
||||||
var newDagJson []byte
|
|
||||||
|
|
||||||
//go:embed testdata/powerline.dagjson
|
|
||||||
var powerlineDagJson []byte
|
|
||||||
|
|
||||||
//go:embed testdata/root.dagjson
|
|
||||||
var rootDagJson []byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
|
||||||
|
|
||||||
subJectCmd = "/foo/bar"
|
|
||||||
subjectPol = `
|
|
||||||
[
|
|
||||||
["==", ".status", "draft"],
|
|
||||||
["all", ".reviewer",
|
|
||||||
["like", ".email", "*@example.com"]
|
|
||||||
],
|
|
||||||
["any", ".tags",
|
|
||||||
["or", [
|
|
||||||
["==", ".", "news"],
|
|
||||||
["==", ".", "press"]
|
|
||||||
]]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
`
|
|
||||||
|
|
||||||
aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8="
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConstructors(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cmd, err := command.Parse(subJectCmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
pol, err := policy.FromDagJson(subjectPol)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
exp, err := time.Parse(time.RFC3339, "2200-01-01T00:00:00Z")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("New", func(t *testing.T) {
|
|
||||||
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, didtest.PersonaCarol.DID(),
|
|
||||||
delegation.WithNonce([]byte(nonce)),
|
|
||||||
delegation.WithExpiration(exp),
|
|
||||||
delegation.WithMeta("foo", "fooo"),
|
|
||||||
delegation.WithMeta("bar", "barr"),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.False(t, tkn.IsRoot())
|
|
||||||
require.False(t, tkn.IsPowerline())
|
|
||||||
|
|
||||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, newDagJson, data)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Root", func(t *testing.T) {
|
|
||||||
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
|
||||||
delegation.WithNonce([]byte(nonce)),
|
|
||||||
delegation.WithExpiration(exp),
|
|
||||||
delegation.WithMeta("foo", "fooo"),
|
|
||||||
delegation.WithMeta("bar", "barr"),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.True(t, tkn.IsRoot())
|
|
||||||
require.False(t, tkn.IsPowerline())
|
|
||||||
|
|
||||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, rootDagJson, data)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Powerline", func(t *testing.T) {
|
|
||||||
tkn, err := delegation.Powerline(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
|
||||||
delegation.WithNonce([]byte(nonce)),
|
|
||||||
delegation.WithExpiration(exp),
|
|
||||||
delegation.WithMeta("foo", "fooo"),
|
|
||||||
delegation.WithMeta("bar", "barr"),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.False(t, tkn.IsRoot())
|
|
||||||
require.True(t, tkn.IsPowerline())
|
|
||||||
|
|
||||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, powerlineDagJson, data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncryptedMeta(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cmd, err := command.Parse(subJectCmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
pol, err := policy.FromDagJson(subjectPol)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
encryptionKey, err := base64.StdEncoding.DecodeString(aesKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, encryptionKey, 32)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
key string
|
|
||||||
value string
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple string",
|
|
||||||
key: "secret1",
|
|
||||||
value: "hello world",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty string",
|
|
||||||
key: "secret2",
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "special characters",
|
|
||||||
key: "secret3",
|
|
||||||
value: "!@#$%^&*()_+-=[]{}|;:,.<>?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unicode characters",
|
|
||||||
key: "secret4",
|
|
||||||
value: "Hello, 世界! 👋",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
|
||||||
delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
decodedTkn, _, err := delegation.FromSealed(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = decodedTkn.Meta().GetString(tt.key)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
decrypted, err := decodedTkn.Meta().GetEncryptedString(tt.key, encryptionKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
// Verify the decrypted value is equal to the original
|
|
||||||
require.Equal(t, tt.value, decrypted)
|
|
||||||
|
|
||||||
// Try to decrypt with wrong key
|
|
||||||
wrongKey := make([]byte, 32)
|
|
||||||
_, err = decodedTkn.Meta().GetEncryptedString(tt.key, wrongKey)
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("multiple encrypted values in the same token", func(t *testing.T) {
|
|
||||||
values := map[string]string{
|
|
||||||
"secret1": "value1",
|
|
||||||
"secret2": "value2",
|
|
||||||
"secret3": "value3",
|
|
||||||
}
|
|
||||||
var opts []delegation.Option
|
|
||||||
for k, v := range values {
|
|
||||||
opts = append(opts, delegation.WithEncryptedMetaString(k, v, encryptionKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create token with multiple encrypted values
|
|
||||||
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
decodedTkn, _, err := delegation.FromSealed(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
for k, v := range values {
|
|
||||||
decrypted, err := decodedTkn.Meta().GetEncryptedString(k, encryptionKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, v, decrypted)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# delegationtest
|
|
||||||
|
|
||||||
See the package documentation for instructions on how to use the generated
|
|
||||||
tokens as well as information on how to regenerate the code if changes have
|
|
||||||
been made.
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user