Compare commits
396 Commits
envelope
...
attestatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b8fb6d34d | ||
|
|
4b99c9f1df | ||
|
|
39694340cd | ||
|
|
e93e464977 | ||
|
|
823af27272 | ||
|
|
70f039a654 | ||
|
|
d56db6722c | ||
|
|
0647e4ff8a | ||
|
|
06f478b9c3 | ||
|
|
4aedc4de39 | ||
|
|
0fd71612d3 | ||
|
|
29ccdb700e | ||
|
|
2d031fdbdb | ||
|
|
1c4a0a9c81 | ||
|
|
df6dfee210 | ||
|
|
c670433335 | ||
|
|
55f38fef4a | ||
|
|
1098a834fb | ||
|
|
b95e525cfb | ||
|
|
cc207aa202 | ||
|
|
41d679dfab | ||
|
|
cc661f3936 | ||
|
|
07d2745966 | ||
|
|
cd9ee535ad | ||
|
|
9e062b0cc7 | ||
|
|
cf3eb1b3f7 | ||
|
|
3b6d70f47a | ||
|
|
09c8815755 | ||
|
|
f18ae547ab | ||
|
|
ad02aa8d4f | ||
|
|
9c8e9f17fa | ||
|
|
11b4352063 | ||
|
|
b0783bf4a4 | ||
|
|
2eeaaccc6d | ||
|
|
1187674a24 | ||
|
|
4c08b22c61 | ||
|
|
174bf01c64 | ||
|
|
4167bf44bd | ||
|
|
4f4331b677 | ||
|
|
547416e60d | ||
|
|
6c1602507b | ||
|
|
6f4853cd2f | ||
|
|
4c25456583 | ||
|
|
83f3e4c3b0 | ||
|
|
1178e51b18 | ||
|
|
3ec8f56412 | ||
|
|
5891bdcd5d | ||
|
|
6fb25481ce | ||
|
|
e7edccdd71 | ||
|
|
33e8a8a821 | ||
|
|
947add66c5 | ||
|
|
3faf9d598c | ||
|
|
fbf55e98ba | ||
|
|
05c2573d95 | ||
|
|
d7472621ce | ||
|
|
7b44f480ee | ||
|
|
5eb7b1a8e4 | ||
|
|
e91afe29d8 | ||
|
|
a82bce556f | ||
|
|
a54d66afe5 | ||
|
|
14a57d7391 | ||
|
|
68469db91a | ||
|
|
cc1d68be0c | ||
|
|
6d3846ac62 | ||
|
|
6aa33b1547 | ||
|
|
9589cc8b44 | ||
|
|
879c0ab03b | ||
|
|
fe14765c8d | ||
|
|
1b28cb49bf | ||
|
|
e1fc838caf | ||
|
|
f29b9e94fc | ||
|
|
506ed21b94 | ||
|
|
126177b9e5 | ||
|
|
2bddab8b0c | ||
|
|
45ead12131 | ||
|
|
9d5e170409 | ||
|
|
9d047f038d | ||
|
|
4c5afcb084 | ||
|
|
e218b49577 | ||
|
|
4c81ac778e | ||
|
|
7ae65e7c8e | ||
|
|
7f9cb6426c | ||
|
|
9c141029c3 | ||
|
|
0c6717cfbc | ||
|
|
6d7fd28324 | ||
|
|
ccc0120e78 | ||
|
|
bf27b97a57 | ||
|
|
10dd4fa6d1 | ||
|
|
5695609f8b | ||
|
|
b7ddead7c8 | ||
|
|
9cbec37685 | ||
|
|
00dbd975b1 | ||
|
|
c792a4cce5 | ||
|
|
8f3f1c775e | ||
|
|
704ed25768 | ||
|
|
95bdbc4fc5 | ||
|
|
416345dba9 | ||
|
|
042d6dc52f | ||
|
|
8bb3a4f4d0 | ||
|
|
47156a8ad6 | ||
|
|
ce6d163627 | ||
|
|
c3c2c96008 | ||
|
|
903632695f | ||
|
|
f2d75b7815 | ||
|
|
4f09829abe | ||
|
|
5660df32b5 | ||
|
|
2f2a74c7ec | ||
|
|
0592717637 | ||
|
|
80c2d60ab3 | ||
|
|
c518c6657a | ||
|
|
78825f4f55 | ||
|
|
7f1adbd945 | ||
|
|
0f59088d0b | ||
|
|
72e0f353e7 | ||
|
|
d0d4ec3abe | ||
|
|
bb24081b28 | ||
|
|
3688ccea01 | ||
|
|
e9105896d7 | ||
|
|
15751c7362 | ||
|
|
d52218fa5a | ||
|
|
64d3024dec | ||
|
|
78d37d92ef | ||
|
|
da806b1bc5 | ||
|
|
311b942a6d | ||
|
|
56eab758ed | ||
|
|
105323b989 | ||
|
|
5b816ccc62 | ||
|
|
28272e6900 | ||
|
|
a854389e32 | ||
|
|
117a75e2c4 | ||
|
|
a25bfbaf45 | ||
|
|
bff482f73b | ||
|
|
ff79bbb443 | ||
|
|
3997a86184 | ||
|
|
200d6a8ae2 | ||
|
|
0349e7e463 | ||
|
|
dff52f80c4 | ||
|
|
5b7a63a2c6 | ||
|
|
66675f7030 | ||
|
|
7e54be49e1 | ||
|
|
15535d3474 | ||
|
|
170e597e71 | ||
|
|
ce1a4b6e32 | ||
|
|
d1d047cd9e | ||
|
|
3680637090 | ||
|
|
1166a68e5c | ||
|
|
ba4db9bce8 | ||
|
|
20369dba49 | ||
|
|
ade2c7f858 | ||
|
|
943a318b26 | ||
|
|
2d79cdc54e | ||
|
|
60bdc8873b | ||
|
|
820057e41e | ||
|
|
ba0038b0ae | ||
|
|
4a4b200312 | ||
|
|
caae2f58bf | ||
|
|
ec627138cb | ||
|
|
4ec409edc6 | ||
|
|
c61fc8d8b3 | ||
|
|
d90715d1fe | ||
|
|
5f8536e480 | ||
|
|
c19e38356d | ||
|
|
aea1880386 | ||
|
|
2fb5a3dc01 | ||
|
|
e980d6c0b9 | ||
|
|
1098e76cba | ||
|
|
bb36d61d93 | ||
|
|
417ef78570 | ||
|
|
00d2380f14 | ||
|
|
8ca088bf27 | ||
|
|
25ca34923f | ||
|
|
fc4c8f2de1 | ||
|
|
64b989452f | ||
|
|
92065ca0d3 | ||
|
|
814cec1495 | ||
|
|
0f70557309 | ||
|
|
89e4d5d419 | ||
|
|
9057cbcba6 | ||
|
|
98d9cadcbd | ||
|
|
e938d64220 | ||
|
|
c577d73f3e | ||
|
|
be185a8496 | ||
|
|
17a57c622a | ||
|
|
6298fa28bd | ||
|
|
d3e97aaa08 | ||
|
|
fdff79d23a | ||
|
|
a26d836025 | ||
|
|
9f47418bdf | ||
|
|
81c7a0f80d | ||
|
|
3987e8649c | ||
|
|
17a1d54b6f | ||
|
|
7cb0f97b30 | ||
|
|
c4a53f42b6 | ||
|
|
522181b16a | ||
|
|
633b3d210a | ||
|
|
3c705ca150 | ||
|
|
1fa2b5e6fc | ||
|
|
11bc085c60 | ||
|
|
a4a8634eb8 | ||
|
|
d353dfe652 | ||
|
|
1e5ecdc205 | ||
|
|
f9065d39d8 | ||
|
|
cddade4670 | ||
|
|
948087744d | ||
|
|
bfb93d6988 | ||
|
|
cfcb199818 | ||
|
|
85557ab6b5 | ||
|
|
adc2b8d0da | ||
|
|
bcdaf0cca3 | ||
|
|
d754c5837b | ||
|
|
d89fb395e3 | ||
|
|
4932e32052 | ||
|
|
a52b48cf47 | ||
|
|
e6e4d85381 | ||
|
|
962e897ff5 | ||
|
|
58bb5cdb8f | ||
|
|
ce7f653ab0 | ||
|
|
7d4f973171 | ||
|
|
3dc0011628 | ||
|
|
08f821f23d | ||
|
|
1b61f2e4db | ||
|
|
187e7a869c | ||
|
|
a98653b769 | ||
|
|
31d16ac468 | ||
|
|
d2b004c405 | ||
|
|
884d63a689 | ||
|
|
c9f3a6033a | ||
|
|
8447499c5a | ||
|
|
b4e222f8a0 | ||
|
|
41b8600fbc | ||
|
|
824c8fe523 | ||
|
|
6aeb6a8b70 | ||
|
|
a1aaf47d7c | ||
|
|
728696f169 | ||
|
|
cfb4446a05 | ||
|
|
06a72868a5 | ||
|
|
6f9a6fa5c1 | ||
|
|
f2b4c3ac20 | ||
|
|
7a7db684c3 | ||
|
|
d7454156d2 | ||
|
|
d3ad6715d9 | ||
|
|
602bdf9c7a | ||
|
|
d21c17c4ca | ||
|
|
72f4ef7b5e | ||
|
|
02be4010d6 | ||
|
|
61e031529f | ||
|
|
19721027e4 | ||
|
|
bc847ee027 | ||
|
|
5bfe430934 | ||
|
|
10b5e1e603 | ||
|
|
3cf1de6b67 | ||
|
|
400f689a85 | ||
|
|
6717a3a89c | ||
|
|
9e9c632ded | ||
|
|
b210c69173 | ||
|
|
6d85b2ba3c | ||
|
|
76c015e78b | ||
|
|
fcb527cc52 | ||
|
|
89f648a94e | ||
|
|
e1d771333c | ||
|
|
f44cf8af78 | ||
|
|
2b2fc4a13f | ||
|
|
6b72799818 | ||
|
|
ccc85d4697 | ||
|
|
0d63e90b67 | ||
|
|
d784c92c29 | ||
|
|
7662fe34db | ||
|
|
6d0fbd4d5a | ||
|
|
e76354fb0a | ||
|
|
deaf9c4fe9 | ||
|
|
a1c2c5c067 | ||
|
|
2c58fedfd5 | ||
|
|
2ea9f8c93b | ||
|
|
00ff88ef23 | ||
|
|
2ffdf004ac | ||
|
|
a8780f750c | ||
|
|
c70f68b886 | ||
|
|
a27eb258e5 | ||
|
|
866683347f | ||
|
|
2fafbe7bf3 | ||
|
|
1728bf29b8 | ||
|
|
8fac97b7e7 | ||
|
|
7ad940844c | ||
|
|
52ae2eaf60 | ||
|
|
570bcdcb6c | ||
|
|
5abb870462 | ||
|
|
4ec675861d | ||
|
|
b941b507e0 | ||
|
|
e66beb662e | ||
|
|
87e25090bb | ||
|
|
6011f0740a | ||
|
|
2bd177ce4d | ||
|
|
abda49061d | ||
|
|
fb978ee574 | ||
|
|
da1310b78a | ||
|
|
ac1b03f144 | ||
|
|
7fa3ba1492 | ||
|
|
081d382028 | ||
|
|
2ad3aeb6da | ||
|
|
030db7ec0d | ||
|
|
aa4ad2fc10 | ||
|
|
51e8d5ce04 | ||
|
|
f8b5fa3a32 | ||
|
|
59da2d1a2c | ||
|
|
88ed55b252 | ||
|
|
9051e5250b | ||
|
|
5f2877f0ff | ||
|
|
100a510097 | ||
|
|
2a51d61b46 | ||
|
|
3e3c5a83cc | ||
|
|
d60fb71156 | ||
|
|
40639b6715 | ||
|
|
60922ced96 | ||
|
|
f7b4b48791 | ||
|
|
346efbd31d | ||
|
|
df9beadf9c | ||
|
|
8615f6c72b | ||
|
|
d9739a3bab | ||
|
|
6b8fbcee0a | ||
|
|
a7037dbc47 | ||
|
|
50ea43e3fa | ||
|
|
0ec16a085c | ||
|
|
2b45f7630e | ||
|
|
637973b10b | ||
|
|
bb4725d87c | ||
|
|
8782554a7b | ||
|
|
a8302ad441 | ||
|
|
b4dd8c0757 | ||
|
|
4201ab2dca | ||
|
|
1b7059c029 | ||
|
|
f3a5209cec | ||
|
|
a2822f02c7 | ||
|
|
79955057a3 | ||
|
|
59cebf8e74 | ||
|
|
952a6bb922 | ||
|
|
2089aa2a6a | ||
|
|
4a655506f9 | ||
|
|
93dd3ef719 | ||
|
|
6075c19957 | ||
|
|
6161f2e440 | ||
|
|
5202056cc7 | ||
|
|
f779477118 | ||
|
|
4974fed931 | ||
|
|
0d9955b7b0 | ||
|
|
6dd6f8a229 | ||
|
|
5509cce513 | ||
|
|
f4ad97679c | ||
|
|
41c8bc7218 | ||
|
|
371bf3b9f5 | ||
|
|
b14671009c | ||
|
|
043c9b160d | ||
|
|
130168809b | ||
|
|
20886f1b5f | ||
|
|
684c21c7a4 | ||
|
|
4749243e3c | ||
|
|
c7f6034376 | ||
|
|
55070dcb43 | ||
|
|
fe594e9906 | ||
|
|
0781b84937 | ||
|
|
f44b6ec2c3 | ||
|
|
abe8a8150a | ||
|
|
f44b5cb921 | ||
|
|
baf3edcf88 | ||
|
|
7107d6bc85 | ||
|
|
c66dd5b2a4 | ||
|
|
70dc12d68e | ||
|
|
dd1f54694f | ||
|
|
ac73cae3ec | ||
|
|
a19d3505fe | ||
|
|
526a34b45d | ||
|
|
989f409fd0 | ||
|
|
40488dfc3d | ||
|
|
84122e57bc | ||
|
|
4e15349c5e | ||
|
|
53cb82a2b4 | ||
|
|
64936fd061 | ||
|
|
30be95b20c | ||
|
|
16ba4b392d | ||
|
|
94a0d4d56e | ||
|
|
53ef97231d | ||
|
|
c960481a10 | ||
|
|
d4d4514971 | ||
|
|
282db65900 | ||
|
|
2459f1a5c3 | ||
|
|
37f5286315 | ||
|
|
06e0674c46 | ||
|
|
ad03154b6e | ||
|
|
700f130858 | ||
|
|
d57d2a230b | ||
|
|
7060d4bb33 | ||
|
|
a183b627be | ||
|
|
dbfff3f70c | ||
|
|
cb45d9019b | ||
|
|
2e17ff8550 | ||
|
|
e86e45be73 | ||
|
|
97c9990045 |
34
.github/workflows/bench.yml
vendored
Normal file
34
.github/workflows/bench.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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.21.x", "1.22.x", "1.23.x", ]
|
go: [ "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@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
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 ./...
|
run: go test -v ./... -tags jwx_es256k
|
||||||
- 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
|
||||||
http://www.apache.org/licenses/
|
https://www.apache.org/licenses/
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
|||||||
76
Readme.md
Normal file
76
Readme.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/ucan-wg/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://github.com/ucan-wg/go-ucan/tags">
|
||||||
|
<img alt="GitHub Tag" src="https://img.shields.io/github/v/tag/ucan-wg/go-ucan">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan/actions?query=">
|
||||||
|
<img src="https://github.com/ucan-wg/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://github.com/ucan-wg/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/github.com/ucan-wg/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://github.com/MetaMask/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://github.com/ucan-wg/go-ucan/blob/v1/LICENSE.md).
|
||||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -1,52 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
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/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 := matchStatement(s.statement, n)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case KindAny:
|
|
||||||
if s, ok := statement.(quantifier); ok {
|
|
||||||
// FIXME: line below return a single node, not many
|
|
||||||
_, many, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || many == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, n := range many {
|
|
||||||
ok := matchStatement(s.statement, 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 }
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
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/capability/policy/literal"
|
|
||||||
"github.com/ucan-wg/go-ucan/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(60)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPolicyExamples(t *testing.T) {
|
|
||||||
makeNode := func(data string) ipld.Node {
|
|
||||||
nd, err := ipld.Decode([]byte(data), dagjson.Decode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
return nd
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluate := func(statement string, data ipld.Node) bool {
|
|
||||||
// we need to wrap statement with [] to make them a policy
|
|
||||||
policy := fmt.Sprintf("[%s]", statement)
|
|
||||||
|
|
||||||
pol, err := FromDagJson(policy)
|
|
||||||
require.NoError(t, err)
|
|
||||||
return Match(pol, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("And", func(t *testing.T) {
|
|
||||||
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
|
|
||||||
|
|
||||||
require.True(t, evaluate(`["and", []]`, data))
|
|
||||||
require.True(t, evaluate(`
|
|
||||||
["and", [
|
|
||||||
["==", ".name", "Katie"],
|
|
||||||
[">=", ".age", 21]
|
|
||||||
]]`, data))
|
|
||||||
require.False(t, evaluate(`
|
|
||||||
["and", [
|
|
||||||
["==", ".name", "Katie"],
|
|
||||||
[">=", ".age", 21],
|
|
||||||
["==", ".nationalities", ["American"]]
|
|
||||||
]]`, data))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Or", func(t *testing.T) {
|
|
||||||
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
|
|
||||||
|
|
||||||
require.True(t, evaluate(`["or", []]`, data))
|
|
||||||
require.True(t, evaluate(`
|
|
||||||
["or", [
|
|
||||||
["==", ".name", "Katie"],
|
|
||||||
[">", ".age", 45]
|
|
||||||
]]
|
|
||||||
`, data))
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Not", func(t *testing.T) {
|
|
||||||
data := makeNode(`{ "name": "Katie", "nationalities": ["Canadian", "South African"] }`)
|
|
||||||
|
|
||||||
require.True(t, evaluate(`
|
|
||||||
["not",
|
|
||||||
["and", [
|
|
||||||
["==", ".name", "Katie"],
|
|
||||||
["==", ".nationalities", ["American"]]
|
|
||||||
]]
|
|
||||||
]
|
|
||||||
`, data))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("All", func(t *testing.T) {
|
|
||||||
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
|
|
||||||
|
|
||||||
require.False(t, evaluate(`["all", ".a", [">", ".b", 0]]`, data))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Any", func(t *testing.T) {
|
|
||||||
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
|
|
||||||
|
|
||||||
require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
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/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{kind: KindEqual, selector: selector, value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{kind: KindGreaterThan, selector: selector, value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{kind: KindGreaterThanOrEqual, selector: selector, value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LessThan(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{kind: KindLessThan, selector: selector, value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{kind: KindLessThanOrEqual, selector: selector, value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
type negation struct {
|
|
||||||
statement Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n negation) Kind() string {
|
|
||||||
return KindNot
|
|
||||||
}
|
|
||||||
|
|
||||||
func Not(stmt Statement) Statement {
|
|
||||||
return negation{statement: stmt}
|
|
||||||
}
|
|
||||||
|
|
||||||
type connective struct {
|
|
||||||
kind string
|
|
||||||
statements []Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c connective) Kind() string {
|
|
||||||
return c.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func And(stmts ...Statement) Statement {
|
|
||||||
return connective{kind: KindAnd, statements: stmts}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Or(stmts ...Statement) Statement {
|
|
||||||
return connective{kind: KindOr, statements: 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: selector, pattern: pattern, glob: g}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type quantifier struct {
|
|
||||||
kind string
|
|
||||||
selector selector.Selector
|
|
||||||
statement Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n quantifier) Kind() string {
|
|
||||||
return n.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func All(selector selector.Selector, statement Statement) Statement {
|
|
||||||
return quantifier{kind: KindAll, selector: selector, statement: statement}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Any(selector selector.Selector, statement Statement) Statement {
|
|
||||||
return quantifier{kind: KindAny, selector: selector, statement: statement}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
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}
|
|
||||||
}
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
if idx < 0 {
|
|
||||||
// necessary until https://github.com/ipld/go-ipld-prime/pull/571
|
|
||||||
// after, isMissing() below will work
|
|
||||||
// TODO: remove
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
|
||||||
}
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
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,46 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
// Code generated by github.com/selesy/go-options. DO NOT EDIT.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Option func(c *config) error
|
|
||||||
|
|
||||||
func newConfig(options ...Option) (config, error) {
|
|
||||||
var c config
|
|
||||||
err := applyConfigOptions(&c, options...)
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyConfigOptions(c *config, options ...Option) error {
|
|
||||||
for _, o := range options {
|
|
||||||
if err := o(c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithExpiration(o *time.Time) Option {
|
|
||||||
return func(c *config) error {
|
|
||||||
c.Expiration = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithMeta(o map[string]datamodel.Node) Option {
|
|
||||||
return func(c *config) error {
|
|
||||||
c.Meta = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithNotBefore(o *time.Time) Option {
|
|
||||||
return func(c *config) error {
|
|
||||||
c.NotBefore = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/ucan-wg/go-ucan/capability/command"
|
|
||||||
"github.com/ucan-wg/go-ucan/capability/policy"
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/envelope"
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/token"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Tag = "ucan/dlg@1.0.0-rc.1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Delegation struct {
|
|
||||||
envel *envelope.Envelope
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:generate -command options go run github.com/selesy/go-options
|
|
||||||
//go:generate options -type=config -prefix=With -output=delegatiom_options.go -cmp=false -stringer=false -imports=time,github.com/ipld/go-ipld-prime/datamodel
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
Expiration *time.Time
|
|
||||||
Meta map[string]datamodel.Node
|
|
||||||
NotBefore *time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(privKey crypto.PrivKey, aud did.DID, sub *did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, opts ...Option) (*Delegation, error) {
|
|
||||||
cfg, err := newConfig(opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
issuer, err := did.FromPrivKey(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !aud.Defined() {
|
|
||||||
return nil, fmt.Errorf("%w: %s", token.ErrMissingRequiredDID, "aud")
|
|
||||||
}
|
|
||||||
audience := aud.String()
|
|
||||||
|
|
||||||
var subject *string
|
|
||||||
if sub != nil {
|
|
||||||
s := sub.String()
|
|
||||||
subject = &s
|
|
||||||
}
|
|
||||||
|
|
||||||
policy, err := pol.ToIPLD()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var meta *token.Map__String__Any
|
|
||||||
if len(cfg.Meta) > 0 {
|
|
||||||
m := token.ToIPLDMapStringAny(cfg.Meta)
|
|
||||||
meta = &m
|
|
||||||
}
|
|
||||||
|
|
||||||
var notBefore *int
|
|
||||||
if cfg.NotBefore != nil {
|
|
||||||
n := int(cfg.NotBefore.Unix())
|
|
||||||
notBefore = &n
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiration *int
|
|
||||||
if cfg.Expiration != nil {
|
|
||||||
e := int(cfg.Expiration.Unix())
|
|
||||||
expiration = &e
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn := &token.Token{
|
|
||||||
Issuer: issuer.String(),
|
|
||||||
Audience: &audience,
|
|
||||||
Subject: subject,
|
|
||||||
Command: cmd.String(),
|
|
||||||
Policy: &policy,
|
|
||||||
Nonce: &nonce,
|
|
||||||
Meta: meta,
|
|
||||||
NotBefore: notBefore,
|
|
||||||
Expiration: expiration,
|
|
||||||
}
|
|
||||||
|
|
||||||
envel, err := envelope.New(privKey, tkn, Tag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dlg := &Delegation{envel: envel}
|
|
||||||
|
|
||||||
if err := dlg.Validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return dlg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Root(privKey crypto.PrivKey, aud did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, opts ...Option) (*Delegation, error) {
|
|
||||||
sub, err := did.FromPrivKey(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return New(privKey, aud, &sub, cmd, pol, nonce, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Audience() did.DID {
|
|
||||||
id, _ := did.Parse(*d.envel.TokenPayload().Audience)
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Command() *command.Command {
|
|
||||||
cmd, _ := command.Parse(d.envel.TokenPayload().Command)
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) IsPowerline() bool {
|
|
||||||
return d.envel.TokenPayload().Subject == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) IsRoot() bool {
|
|
||||||
return &d.envel.TokenPayload().Issuer == d.envel.TokenPayload().Subject
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Issuer() did.DID {
|
|
||||||
id, _ := did.Parse(d.envel.TokenPayload().Issuer)
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Meta() map[string]datamodel.Node {
|
|
||||||
return d.envel.TokenPayload().Meta.Values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Nonce() []byte {
|
|
||||||
return *d.envel.TokenPayload().Nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Policy() policy.Policy {
|
|
||||||
pol, _ := policy.FromIPLD(*d.envel.TokenPayload().Policy)
|
|
||||||
|
|
||||||
return pol
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Subject() *did.DID {
|
|
||||||
if d.envel.TokenPayload().Subject == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
id, _ := did.Parse(*d.envel.TokenPayload().Subject)
|
|
||||||
|
|
||||||
return &id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) Validate() error {
|
|
||||||
return errors.Join(
|
|
||||||
d.validateDID("iss", &d.envel.TokenPayload().Issuer, false),
|
|
||||||
d.validateDID("aud", d.envel.TokenPayload().Audience, false),
|
|
||||||
d.validateDID("sub", d.envel.TokenPayload().Subject, true),
|
|
||||||
d.validateCommand(),
|
|
||||||
d.validatePolicy(),
|
|
||||||
d.validateNonce(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) validateCommand() error {
|
|
||||||
_, err := command.Parse(d.envel.TokenPayload().Command)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) validateDID(fieldName string, identity *string, nullableOrOptional bool) error {
|
|
||||||
if identity == nil && !nullableOrOptional {
|
|
||||||
return fmt.Errorf("a required DID is missing: %s", fieldName)
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := did.Parse(*identity)
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if !id.Defined() && !id.Key() {
|
|
||||||
return fmt.Errorf("a required DID is missing: %s", fieldName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) validateNonce() error {
|
|
||||||
if d.envel.TokenPayload().Nonce == nil || len(*d.envel.TokenPayload().Nonce) < 1 {
|
|
||||||
return fmt.Errorf("nonce is required: must not be nil or empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Delegation) validatePolicy() error {
|
|
||||||
if d.envel.TokenPayload().Policy == nil {
|
|
||||||
return fmt.Errorf("the \"pol\" field is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := policy.FromIPLD(*d.envel.TokenPayload().Policy)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Nonce() ([]byte, error) {
|
|
||||||
nonce := make([]byte, 32)
|
|
||||||
|
|
||||||
if _, err := rand.Read(nonce); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nonce, nil
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
package delegation_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/ucan-wg/go-ucan/capability/command"
|
|
||||||
"github.com/ucan-wg/go-ucan/capability/policy"
|
|
||||||
"github.com/ucan-wg/go-ucan/delegation"
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"gotest.tools/v3/golden"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
|
||||||
|
|
||||||
AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM="
|
|
||||||
AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv"
|
|
||||||
|
|
||||||
issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU="
|
|
||||||
issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
|
|
||||||
|
|
||||||
subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c="
|
|
||||||
subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
|
||||||
subJectCmd = "/foo/bar"
|
|
||||||
subjectPol = `
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"==",
|
|
||||||
".status",
|
|
||||||
"draft"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"all",
|
|
||||||
".reviewer",
|
|
||||||
[
|
|
||||||
"like",
|
|
||||||
".email",
|
|
||||||
"*@example.com"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"any",
|
|
||||||
".tags",
|
|
||||||
[
|
|
||||||
"or",
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"==",
|
|
||||||
".",
|
|
||||||
"news"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"==",
|
|
||||||
".",
|
|
||||||
"press"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConstructors(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
privKey := privKey(t, issuerPrivKeyCfg)
|
|
||||||
|
|
||||||
aud, err := did.Parse(AudienceDID)
|
|
||||||
|
|
||||||
sub, err := did.Parse(subjectDID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cmd, err := command.Parse(subJectCmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
pol, err := policy.FromDagJson(subjectPol)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
exp := time.Time{}
|
|
||||||
|
|
||||||
meta := map[string]datamodel.Node{
|
|
||||||
"foo": basicnode.NewString("fooo"),
|
|
||||||
"bar": basicnode.NewString("barr"),
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("New", func(t *testing.T) {
|
|
||||||
dlg, err := delegation.New(privKey, aud, &sub, cmd, pol, []byte(nonce), delegation.WithExpiration(&exp), delegation.WithMeta(meta))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
data, err := dlg.ToDagJson()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Log(string(data))
|
|
||||||
|
|
||||||
golden.Assert(t, string(data), "new.dagjson")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Root", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
dlg, err := delegation.Root(privKey, aud, cmd, pol, []byte(nonce), delegation.WithExpiration(&exp), delegation.WithMeta(meta))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
data, err := dlg.ToDagJson()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Log(string(data))
|
|
||||||
|
|
||||||
golden.Assert(t, string(data), "root.dagjson")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func privKey(t *testing.T, privKeyCfg string) crypto.PrivKey {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return privKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKey(t *testing.T) {
|
|
||||||
t.Skip()
|
|
||||||
|
|
||||||
priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
privMar, err := crypto.MarshalPrivateKey(priv)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
privCfg := crypto.ConfigEncodeKey(privMar)
|
|
||||||
t.Log(privCfg)
|
|
||||||
|
|
||||||
id, err := did.FromPubKey(priv.GetPublic())
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Log(id)
|
|
||||||
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Encode marshals a Delegation to the format specified by the provided
|
|
||||||
// codec.Encoder.
|
|
||||||
func (d *Delegation) Encode(encFn codec.Encoder) ([]byte, error) {
|
|
||||||
node, err := d.ToIPLD()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipld.Encode(node, encFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagCbor marshals the Delegation to the DAG-CBOR format.
|
|
||||||
func (d *Delegation) ToDagCbor() ([]byte, error) {
|
|
||||||
return d.Encode(dagcbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagJson marshals the Delegation to the DAG-JSON format.
|
|
||||||
func (d *Delegation) ToDagJson() ([]byte, error) {
|
|
||||||
return d.Encode(dagjson.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToIPLD wraps the Delegation in an IPLD datamodel.Node.
|
|
||||||
func (d *Delegation) ToIPLD() (datamodel.Node, error) {
|
|
||||||
return d.envel.Wrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode unmarshals the input data using the format specified by the
|
|
||||||
// provided codec.Decoder into a Delegation.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Delegation is invalid.
|
|
||||||
func Decode(b []byte, decFn codec.Decoder) (*Delegation, error) {
|
|
||||||
node, err := ipld.Decode(b, decFn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return FromIPLD(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeReader is the same as Decode, but accept an io.Reader.
|
|
||||||
func DecodeReader(r io.Reader, decFn codec.Decoder) (*Delegation, error) {
|
|
||||||
node, err := ipld.DecodeStreaming(r, decFn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return FromIPLD(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCbor unmarshals the input data into a Delegation.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Delegation is invalid.
|
|
||||||
func FromDagCbor(data []byte) (*Delegation, error) {
|
|
||||||
return Decode(data, dagcbor.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
|
||||||
func FromDagCborReader(r io.Reader) (*Delegation, error) {
|
|
||||||
return DecodeReader(r, dagcbor.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJson unmarshals the input data into a Delegation.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Delegation is invalid.
|
|
||||||
func FromDagJson(data []byte) (*Delegation, error) {
|
|
||||||
return Decode(data, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
|
||||||
func FromDagJsonReader(r io.Reader) (*Delegation, error) {
|
|
||||||
return DecodeReader(r, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromIPLD unwraps a Delegation from the provided IPLD datamodel.Node
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Delegation is invalid.
|
|
||||||
func FromIPLD(node datamodel.Node) (*Delegation, error) {
|
|
||||||
envel, err := envelope.Unwrap(node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if envel.Tag() != Tag {
|
|
||||||
return nil, fmt.Errorf("wrong tag for TokenPayload: received %s but expected %s", envel.Tag(), Tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
dlg := &Delegation{
|
|
||||||
envel: envel,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dlg.Validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return dlg, nil
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package delegation_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/ucan-wg/go-ucan/delegation"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEncodingRoundTrip(t *testing.T) {
|
|
||||||
const delegationJson = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"/": {
|
|
||||||
"bytes": "QWr0Pk+sSWE1nszuBMQzggbHX4ofJb8QRdwrLJK/AGCx2p4s/xaCRieomfstDjsV4ezBzX1HARvcoNgdwDQ8Aw"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"h": {
|
|
||||||
"/": {
|
|
||||||
"bytes": "NO0BcQ"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ucan/dlg@1.0.0-rc.1": {
|
|
||||||
"aud": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2",
|
|
||||||
"cmd": "/foo/bar",
|
|
||||||
"exp": -62135596800,
|
|
||||||
"iss": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2",
|
|
||||||
"meta": {
|
|
||||||
"bar": "barr",
|
|
||||||
"foo": "fooo"
|
|
||||||
},
|
|
||||||
"nbf": -62135596800,
|
|
||||||
"nonce": {
|
|
||||||
"/": {
|
|
||||||
"bytes": "X93ORvN1QIXrKPyEP5m5XoVK9VLX9nX8VV/+HlWrp9c"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pol": [
|
|
||||||
[
|
|
||||||
"==",
|
|
||||||
".status",
|
|
||||||
"draft"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"all",
|
|
||||||
".reviewer",
|
|
||||||
[
|
|
||||||
"like",
|
|
||||||
".email",
|
|
||||||
"*@example.com"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"any",
|
|
||||||
".tags",
|
|
||||||
[
|
|
||||||
"or",
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"==",
|
|
||||||
".",
|
|
||||||
"news"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"==",
|
|
||||||
".",
|
|
||||||
"press"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
`
|
|
||||||
// format: dagJson --> Delegation --> dagCbor --> Delegation --> dagJson
|
|
||||||
// function: FromDagJson() ToDagCbor() FromDagCbor() ToDagJson()
|
|
||||||
|
|
||||||
p1, err := delegation.FromDagJson([]byte(delegationJson))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cborBytes, err := p1.ToDagCbor()
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("cborBytes length", len(cborBytes))
|
|
||||||
fmt.Println("cbor", string(cborBytes))
|
|
||||||
|
|
||||||
p2, err := delegation.FromDagCbor(cborBytes)
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("read Cbor", p2)
|
|
||||||
|
|
||||||
readJson, err := p2.ToDagJson()
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("readJson length", len(readJson))
|
|
||||||
fmt.Println("json: ", string(readJson))
|
|
||||||
|
|
||||||
require.JSONEq(t, delegationJson, string(readJson))
|
|
||||||
}
|
|
||||||
1
delegation/testdata/new.dagjson
vendored
1
delegation/testdata/new.dagjson
vendored
@@ -1 +0,0 @@
|
|||||||
[{"/":{"bytes":"P2lPLfdMuZuc4NPZ0mbozU+/bn5xoWlJsu+Fvaxi4ICYXVJb9/wiTTht3WJEFqjxXLxfTl4BMZF3J1CNvMPqBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":-62135596800,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"}}]
|
|
||||||
1
delegation/testdata/root.dagjson
vendored
1
delegation/testdata/root.dagjson
vendored
@@ -1 +0,0 @@
|
|||||||
[{"/":{"bytes":"0sjiwG9BOgpezz6qw5UiD+rqOeqFLn4+Qds1PvbnsUBoc3RhF6IVxIeoOXDh1ufv3RHaI/zg4wjYpUwAMpTACw"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":-62135596800,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"}}]
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package did
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
|
||||||
"github.com/multiformats/go-multicodec"
|
|
||||||
"github.com/multiformats/go-varint"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
|
|
||||||
return FromPubKey(privKey.GetPublic())
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromPubKey(pubKey crypto.PubKey) (DID, error) {
|
|
||||||
code, ok := map[pb.KeyType]multicodec.Code{
|
|
||||||
pb.KeyType_Ed25519: multicodec.Ed25519Pub,
|
|
||||||
pb.KeyType_RSA: multicodec.RsaPub,
|
|
||||||
pb.KeyType_Secp256k1: multicodec.Secp256k1Pub,
|
|
||||||
pb.KeyType_ECDSA: multicodec.Es256,
|
|
||||||
}[pubKey.Type()]
|
|
||||||
if !ok {
|
|
||||||
return Undef, errors.New("Blah")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := varint.ToUvarint(uint64(code))
|
|
||||||
|
|
||||||
pubBytes, err := pubKey.Raw()
|
|
||||||
if err != nil {
|
|
||||||
return Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return DID{
|
|
||||||
str: string(append(buf, pubBytes...)),
|
|
||||||
code: uint64(code),
|
|
||||||
key: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPubKey(s string) (crypto.PubKey, error) {
|
|
||||||
id, err := Parse(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return id.PubKey()
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package did_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
exampleDIDStr = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
|
|
||||||
examplePubKeyStr = "Lm/M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY="
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFromPubKey(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
id, err := did.FromPubKey(examplePubKey(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, exampleDID(t), id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToPubKey(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
pubKey, err := did.ToPubKey(exampleDIDStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, examplePubKey(t), pubKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleDID(t *testing.T) did.DID {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
id, err := did.Parse(exampleDIDStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func examplePubKey(t *testing.T) crypto.PubKey {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
pubKeyCfg, err := crypto.ConfigDecodeKey(examplePubKeyStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
pubKey, err := crypto.UnmarshalEd25519PublicKey(pubKeyCfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return pubKey
|
|
||||||
}
|
|
||||||
118
did/did.go
118
did/did.go
@@ -1,118 +0,0 @@
|
|||||||
package did
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
mbase "github.com/multiformats/go-multibase"
|
|
||||||
"github.com/multiformats/go-multicodec"
|
|
||||||
varint "github.com/multiformats/go-varint"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Prefix = "did:"
|
|
||||||
const KeyPrefix = "did:key:"
|
|
||||||
|
|
||||||
const DIDCore = 0x0d1d
|
|
||||||
const Ed25519 = 0xed
|
|
||||||
const RSA = uint64(multicodec.RsaPub)
|
|
||||||
|
|
||||||
var MethodOffset = varint.UvarintSize(uint64(DIDCore))
|
|
||||||
|
|
||||||
//
|
|
||||||
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
|
|
||||||
type DID struct {
|
|
||||||
key bool
|
|
||||||
code uint64
|
|
||||||
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) Code() uint64 {
|
|
||||||
return d.code
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DID) DID() DID {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DID) Key() bool {
|
|
||||||
return d.key
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DID) PubKey() (crypto.PubKey, error) {
|
|
||||||
if !d.key {
|
|
||||||
return nil, fmt.Errorf("unsupported did type: %s", d.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
|
|
||||||
multicodec.Ed25519Pub: crypto.UnmarshalEd25519PublicKey,
|
|
||||||
multicodec.RsaPub: crypto.UnmarshalRsaPublicKey,
|
|
||||||
multicodec.Secp256k1Pub: crypto.UnmarshalSecp256k1PublicKey,
|
|
||||||
multicodec.Es256: crypto.UnmarshalECDSAPublicKey,
|
|
||||||
}[multicodec.Code(d.code)]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
|
|
||||||
}
|
|
||||||
|
|
||||||
return unmarshaler(d.Bytes()[varint.UvarintSize(d.code):])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 || code == RSA {
|
|
||||||
return DID{str: string(bytes), code: code, 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), code: DIDCore}, nil
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
doc.go
5
doc.go
@@ -1,5 +0,0 @@
|
|||||||
// Package ucan provides the core functionality required to grant and
|
|
||||||
// revoke privileges via [UCAN] tokens.
|
|
||||||
//
|
|
||||||
// [UCAN]: https://ucan.xyz
|
|
||||||
package ucan
|
|
||||||
30
go.mod
30
go.mod
@@ -1,29 +1,27 @@
|
|||||||
module github.com/ucan-wg/go-ucan
|
module github.com/ucan-wg/go-ucan
|
||||||
|
|
||||||
go 1.22.0
|
go 1.24.4
|
||||||
|
|
||||||
toolchain go1.22.4
|
toolchain go1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gobwas/glob v0.2.3
|
github.com/MetaMask/go-did-it v1.0.0-pre1
|
||||||
github.com/ipfs/go-cid v0.4.1
|
github.com/avast/retry-go/v4 v4.6.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/libp2p/go-libp2p v0.36.2
|
|
||||||
github.com/multiformats/go-multibase v0.2.0
|
github.com/multiformats/go-multibase v0.2.0
|
||||||
github.com/multiformats/go-multicodec v0.9.0
|
github.com/multiformats/go-multicodec v0.9.0
|
||||||
github.com/multiformats/go-multihash v0.2.3
|
github.com/multiformats/go-multihash v0.2.3
|
||||||
github.com/multiformats/go-varint v0.0.7
|
github.com/multiformats/go-varint v0.0.7
|
||||||
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/ucan-wg/go-varsig v1.0.0
|
||||||
gotest.tools/v3 v3.5.1
|
golang.org/x/crypto v0.40.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.3.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
github.com/fatih/structtag v1.2.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
|
||||||
github.com/minio/sha256-simd v1.0.1 // 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.1.0 // indirect
|
||||||
@@ -31,12 +29,8 @@ require (
|
|||||||
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/crypto v0.25.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
|
||||||
golang.org/x/tools v0.25.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.34.2 // 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.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
66
go.sum
66
go.sum
@@ -1,38 +1,34 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/MetaMask/go-did-it v1.0.0-pre1 h1:NTGAC7z52TwFegEF7c+csUr/6Al1nAo6ValAAxOsjto=
|
||||||
|
github.com/MetaMask/go-did-it v1.0.0-pre1/go.mod h1:7m9syDnXFTg5GmUEcydpO4Rs3eYT4McFH7vCw5fp3A4=
|
||||||
|
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.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
|
||||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
|
||||||
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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/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.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
|
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||||
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
|
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||||
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.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
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/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
|
||||||
github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U=
|
|
||||||
github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY=
|
|
||||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||||
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=
|
||||||
@@ -41,8 +37,6 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG
|
|||||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||||
github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
|
|
||||||
github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
|
|
||||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||||
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=
|
||||||
@@ -58,8 +52,6 @@ github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX
|
|||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52 h1:poNWlojS+o3229ZuatLMzK9wFiLuLxo7O170Edggs0o=
|
|
||||||
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52/go.mod h1:Cn8TrnJWCWd3dAmejFTpLN8tNVNKNoVVlZzL8ux5EWQ=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
@@ -67,39 +59,29 @@ 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
|
||||||
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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
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=
|
||||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
|
||||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
|
||||||
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=
|
||||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
|
||||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
|
||||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
|
||||||
)
|
|
||||||
|
|
||||||
const header = `// Code generated by internal/cmd/token - DO NOT EDIT.
|
|
||||||
|
|
||||||
package token
|
|
||||||
|
|
||||||
import "github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
slog.Info("Generating Go types for token.ipldsch")
|
|
||||||
|
|
||||||
if err := Run(); err != nil {
|
|
||||||
slog.Error(err.Error())
|
|
||||||
slog.Error("Finished but failed to generate and write token_gen.go")
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("Finished generating and writing token_gen.go")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run() error {
|
|
||||||
schema, err := os.ReadFile("token.ipldsch")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug(string(schema))
|
|
||||||
|
|
||||||
typeSystem, err := ipld.LoadSchemaBytes(schema)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.NewBufferString(header)
|
|
||||||
|
|
||||||
if err := bindnode.ProduceGoTypes(buf, typeSystem); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile("token_gen.go", buf.Bytes(), 0o600)
|
|
||||||
}
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
package envelope
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"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/node/bindnode"
|
|
||||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/token"
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/varsig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// [Envelope] is a signed enclosure for a UCAN v1 Token.
|
|
||||||
//
|
|
||||||
// While the types and functions in this package are not exported,
|
|
||||||
// the names used for types, fields, variables, etc generally use the
|
|
||||||
// names from the specification
|
|
||||||
//
|
|
||||||
// [Envelope]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
type Envelope struct {
|
|
||||||
signature []byte
|
|
||||||
sigPayload *sigPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates an Envelope containing a VarsigHeader and Signature for
|
|
||||||
// the data resulting from wrapping the provided Token in an IPLD
|
|
||||||
// datamodel.Node and encoding it using DAG-CBOR.
|
|
||||||
func New(privKey crypto.PrivKey, token *token.Token, tag string) (*Envelope, error) {
|
|
||||||
sigPayload, err := newSigPayload(privKey.Type(), token, tag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cbor, err := sigPayload.cbor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
signature, err := privKey.Sign(cbor)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Envelope{
|
|
||||||
signature: signature,
|
|
||||||
sigPayload: sigPayload,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap is syntactic sugar for creating an Envelope and wrapping it as an
|
|
||||||
// IPLD datamodel.Node in a single operation.
|
|
||||||
//
|
|
||||||
// Since the Envelope itself isn't returned, use this method only when
|
|
||||||
// the IPLD datamodel.Node is used directly. If the Envelope is also
|
|
||||||
// required, use New followed by Envelope.Wrap to avoid the need to
|
|
||||||
// unwrap the newly created datamodel.Node.
|
|
||||||
func Wrap(privKey crypto.PrivKey, token *token.Token, tag string) (datamodel.Node, error) {
|
|
||||||
env, err := New(privKey, token, tag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return env.Wrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap attempts to crate an Envelope from a datamodel.Node
|
|
||||||
//
|
|
||||||
// There are lots of ways that this can fail and therefore there are
|
|
||||||
// an almost excessive number of check included here and while
|
|
||||||
// attempting to extract the token.Token from one of the inner IPLD
|
|
||||||
// nodes.
|
|
||||||
func Unwrap(node datamodel.Node) (*Envelope, error) {
|
|
||||||
signatureNode, err := node.LookupByIndex(0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
signature, err := signatureNode.AsBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sigPayloadNode, err := node.LookupByIndex(1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sigPayload, err := unwrapSigPayload(sigPayloadNode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
envel := &Envelope{
|
|
||||||
signature: signature,
|
|
||||||
sigPayload: sigPayload,
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok, err := envel.Verify(); !ok || err != nil {
|
|
||||||
return nil, fmt.Errorf("envelope was not signed by issuer")
|
|
||||||
}
|
|
||||||
|
|
||||||
return envel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signature returns the cryptographic signature of the Envelope's
|
|
||||||
// SigPayload.
|
|
||||||
func (e *Envelope) Signature() []byte {
|
|
||||||
return e.signature
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag returns the key that's used to reference the TokenPayload within
|
|
||||||
// this Envelope.
|
|
||||||
func (e *Envelope) Tag() string {
|
|
||||||
return e.sigPayload.tag
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenPayload returns the *token.Token enclosed within this Envelope.
|
|
||||||
func (e *Envelope) TokenPayload() *token.Token {
|
|
||||||
return e.sigPayload.tokenPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
// VarsigHeader is an accessor that returns the [VarsigHeader] from the
|
|
||||||
// underlying [SigPayload] from the [Envelope].
|
|
||||||
//
|
|
||||||
// [Envelope]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
// [SigPayload]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
// [VarsigHeader]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
func (e *Envelope) VarsigHeader() []byte {
|
|
||||||
return e.sigPayload.varsigHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify checks that the [Envelope]'s signature is correct for the
|
|
||||||
// data created by encoding the SigPayload as DAG-CBOR and the public
|
|
||||||
// key passed as the only argument.
|
|
||||||
//
|
|
||||||
// Note that for Delegation and Invocation tokens, the public key
|
|
||||||
// is retrieved from the DID's method specific identifier for the
|
|
||||||
// Issuer field.
|
|
||||||
//
|
|
||||||
// [Envelope]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
func (e *Envelope) Verify() (bool, error) {
|
|
||||||
pubKey, err := did.ToPubKey(e.sigPayload.tokenPayload.Issuer)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cbor, err := e.sigPayload.cbor()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pubKey.Verify(cbor, e.signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap encodes the Envelope as an IPLD datamodel.Node.
|
|
||||||
func (e *Envelope) Wrap() (datamodel.Node, error) {
|
|
||||||
spn, err := e.sigPayload.wrap()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
|
||||||
qp.ListEntry(la, qp.Bytes(e.signature))
|
|
||||||
qp.ListEntry(la, qp.Node(spn))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// The types below are strictly to make it easier to Wrap and Unwrap the
|
|
||||||
// Envelope with an IPLD datamodel.Node. The Envelope itself provides
|
|
||||||
// accessors to the internals of these types.
|
|
||||||
//
|
|
||||||
|
|
||||||
type sigPayload struct {
|
|
||||||
varsigHeader []byte
|
|
||||||
tokenPayload *token.Token
|
|
||||||
tag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSigPayload(keyType pb.KeyType, token *token.Token, tag string) (*sigPayload, error) {
|
|
||||||
varsigHeader, err := varsig.Encode(keyType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &sigPayload{
|
|
||||||
varsigHeader: varsigHeader,
|
|
||||||
tokenPayload: token,
|
|
||||||
tag: tag,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func unwrapSigPayload(node datamodel.Node) (*sigPayload, error) {
|
|
||||||
// Normally we could look up the VarsigHeader and TokenPayload using
|
|
||||||
// node.LookupByString() - this works for the "h" key used for the
|
|
||||||
// VarsigHeader but not for the TokenPayload's key (tag) as all we
|
|
||||||
// know is that it starts with "ucan/" and as explained below, must
|
|
||||||
// decode to a schema.TypedNode for the representation provided by the
|
|
||||||
// token.Prototype().
|
|
||||||
// vvv
|
|
||||||
mi := node.MapIterator()
|
|
||||||
if mi == nil {
|
|
||||||
return nil, fmt.Errorf("the SigPayload node is not a map: %s", node.Kind().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
hdrNode datamodel.Node
|
|
||||||
tknNode datamodel.Node
|
|
||||||
tag string
|
|
||||||
)
|
|
||||||
|
|
||||||
keyCount := 0
|
|
||||||
|
|
||||||
for !mi.Done() {
|
|
||||||
k, v, err := mi.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
kStr, err := k.AsString()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("the SigPayload keys are not strings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyCount++
|
|
||||||
|
|
||||||
if kStr == "h" {
|
|
||||||
hdrNode = v
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(kStr, "ucan/") {
|
|
||||||
tknNode = v
|
|
||||||
tag = kStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if keyCount != 2 {
|
|
||||||
return nil, fmt.Errorf("the SigPayload map should have exactly two keys: %d", keyCount)
|
|
||||||
}
|
|
||||||
// ^^^
|
|
||||||
|
|
||||||
// Replaces the datamodel.Node in tokenPayloadNode with a
|
|
||||||
// schema.TypedNode so that we can cast it to a *token.Token after
|
|
||||||
// unwrapping it.
|
|
||||||
// vvv
|
|
||||||
nb := token.Prototype().Representation().NewBuilder()
|
|
||||||
|
|
||||||
err := nb.AssignNode(tknNode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tknNode = nb.Build()
|
|
||||||
// ^^^
|
|
||||||
|
|
||||||
tokenPayload := bindnode.Unwrap(tknNode)
|
|
||||||
if tokenPayload == nil {
|
|
||||||
return nil, errors.New("failed to Unwrap the TokenPayload")
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn, ok := tokenPayload.(*token.Token)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("failed to assert the TokenPayload type as *token.Token")
|
|
||||||
}
|
|
||||||
|
|
||||||
hdr, err := hdrNode.AsBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &sigPayload{
|
|
||||||
varsigHeader: hdr,
|
|
||||||
tokenPayload: tkn,
|
|
||||||
tag: tag,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sp *sigPayload) cbor() ([]byte, error) {
|
|
||||||
node, err := sp.wrap()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
if err = dagcbor.Encode(node, buf); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sp *sigPayload) wrap() (datamodel.Node, error) {
|
|
||||||
tpn := bindnode.Wrap(sp.tokenPayload, token.Prototype().Type())
|
|
||||||
|
|
||||||
return qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
|
||||||
qp.MapEntry(ma, "h", qp.Bytes(sp.varsigHeader))
|
|
||||||
qp.MapEntry(ma, sp.tag, qp.Node(tpn.Representation()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
package envelope_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"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/node/basicnode"
|
|
||||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/envelope"
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/token"
|
|
||||||
"gotest.tools/v3/golden"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"
|
|
||||||
examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4="
|
|
||||||
exampleSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"
|
|
||||||
exampleTag = "ucan/example@v1.0.0-rc.1"
|
|
||||||
exampleVarsigHeaderStr = "NO0BcQ"
|
|
||||||
|
|
||||||
invalidSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBK"
|
|
||||||
|
|
||||||
exampleDAGCBORFilename = "example.dagcbor"
|
|
||||||
exampleDAGJSONFilename = "example.dagjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
envel := exampleEnvelope(t)
|
|
||||||
assert.NotZero(t, envel)
|
|
||||||
|
|
||||||
assert.Equal(t, exampleSignature(t), envel.Signature())
|
|
||||||
assert.Equal(t, exampleTag, envel.Tag())
|
|
||||||
assert.Equal(t, exampleVarsigHeader(t), envel.VarsigHeader())
|
|
||||||
assert.EqualValues(t, exampleGoldenTokenPayload(t), envel.TokenPayload())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWrap(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
node, err := envelope.Wrap(examplePrivKey(t), exampleToken(t), exampleTag)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cbor, err := ipld.Encode(node, dagcbor.Encode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
golden.AssertBytes(t, cbor, exampleDAGCBORFilename)
|
|
||||||
|
|
||||||
json, err := ipld.Encode(node, dagjson.Encode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
golden.Assert(t, string(json), exampleDAGJSONFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvelope_Verify(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("valid signature by issuer", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
envel := exampleEnvelope(t)
|
|
||||||
ok, err := envel.Verify()
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid signature by wrong issuer", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
envel, err := envelope.Unwrap(invalidNodeFromGolden(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ok, _ := envel.Verify()
|
|
||||||
assert.False(t, ok)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvelope_Wrap(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
envel := exampleEnvelope(t)
|
|
||||||
|
|
||||||
node, err := envel.Wrap()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cbor, err := ipld.Encode(node, dagcbor.Encode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, golden.Get(t, exampleDAGCBORFilename), cbor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleGoldenEnvelope(t *testing.T) *envelope.Envelope {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
envel, err := envelope.Unwrap(exampleGoldenNode(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return envel
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleGoldenNode(t *testing.T) datamodel.Node {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
cbor := golden.Get(t, exampleDAGCBORFilename)
|
|
||||||
|
|
||||||
node, err := ipld.Decode(cbor, dagcbor.Decode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleGoldenTokenPayload(t *testing.T) *token.Token {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
return exampleGoldenEnvelope(t).TokenPayload()
|
|
||||||
}
|
|
||||||
|
|
||||||
func examplePrivKey(t *testing.T) crypto.PrivKey {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
privKeyEnc, err := crypto.ConfigDecodeKey(examplePrivKeyCfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
privKey, err := crypto.UnmarshalPrivateKey(privKeyEnc)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return privKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleEnvelope(t *testing.T) *envelope.Envelope {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
envel, err := envelope.New(examplePrivKey(t), exampleToken(t), exampleTag)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return envel
|
|
||||||
}
|
|
||||||
|
|
||||||
func examplePubKey(t *testing.T) crypto.PubKey {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
return examplePrivKey(t).GetPublic()
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleSignature(t *testing.T) []byte {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
sig, err := base64.RawStdEncoding.DecodeString(exampleSignatureStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return sig
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleToken(t *testing.T) *token.Token {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
id, err := did.FromPubKey(examplePubKey(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return &token.Token{
|
|
||||||
Issuer: id.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleVarsigHeader(t *testing.T) []byte {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
hdr, err := base64.RawStdEncoding.DecodeString(exampleVarsigHeaderStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return hdr
|
|
||||||
}
|
|
||||||
|
|
||||||
func invalidNodeFromGolden(t *testing.T) datamodel.Node {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
invalidSig, err := base64.RawStdEncoding.DecodeString(invalidSignatureStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
envelNode := exampleGoldenNode(t)
|
|
||||||
sigPayloadNode, err := envelNode.LookupByIndex(1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
node, err := qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
|
||||||
qp.ListEntry(la, qp.Bytes(invalidSig))
|
|
||||||
qp.ListEntry(la, qp.Node(sigPayloadNode))
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
1
internal/envelope/testdata/example.dagcbor
vendored
1
internal/envelope/testdata/example.dagcbor
vendored
@@ -1 +0,0 @@
|
|||||||
‚X@=•zfˆîŒ— ©Ê¦Z.ÈÚàP óú¤‹Jr=n–¬;¡)Dñ÷™¹6îB;ò
|
|
||||||
20
internal/envelope/testdata/example.dagjson
vendored
20
internal/envelope/testdata/example.dagjson
vendored
@@ -1,20 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"/": {
|
|
||||||
"bytes": "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"h": {
|
|
||||||
"/": {
|
|
||||||
"bytes": "NO0BcQ"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ucan/example@v1.0.0-rc.1": {
|
|
||||||
"cmd": "",
|
|
||||||
"exp": null,
|
|
||||||
"iss": "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh",
|
|
||||||
"sub": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package token
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ToIPLDMapStringAny(m map[string]datamodel.Node) Map__String__Any {
|
|
||||||
keys := make([]string, len(m))
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
for k := range m {
|
|
||||||
keys[i] = k
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
return Map__String__Any{
|
|
||||||
Keys: keys,
|
|
||||||
Values: m,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromIPLDMapStringAny(m Map__String__Any) map[string]datamodel.Node {
|
|
||||||
return m.Values
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Package token provides a generic model of the [TokenPayload] required
|
|
||||||
// within an Envelope.
|
|
||||||
//
|
|
||||||
// # Field requirements
|
|
||||||
//
|
|
||||||
// While the Token object represents the wire format of both a UCAN
|
|
||||||
// Delegation token and a UCAN Invocation token, the delegation and
|
|
||||||
// invocation packages are, respectively, responsible for making sure
|
|
||||||
// required fields are included when creating a new Token or when
|
|
||||||
// validating the contents of an Envelope as it's received from
|
|
||||||
// another party. The following table shows the current (as of
|
|
||||||
// 2024-09-11) relationship between optional and nullable fields in
|
|
||||||
// the delegation and invocation views and the payload model:
|
|
||||||
//
|
|
||||||
// | Name | Delegation | Invocation | Token |
|
|
||||||
// | | Required | Nullable | Required | Nullable | |
|
|
||||||
// | ----- | -------- | -------- | -------- | -------- | -------- |
|
|
||||||
// | iss | Yes | No | Yes | No | |
|
|
||||||
// | aud | Yes | No | No | N/A | Optional |
|
|
||||||
// | sub | Yes | Yes | Yes | No | Nullable |
|
|
||||||
// | cmd | Yes | No | Yes | No | |
|
|
||||||
// | pol | Yes | No | X | X | Optional |
|
|
||||||
// | nonce | Yes | No | No | N/A | Optional |
|
|
||||||
// | meta | No | N/A | No | N/A | Optional |
|
|
||||||
// | nbf | No | N/A | X | X | Optional |
|
|
||||||
// | exp | Yes | Yes | Yes | Yes | |
|
|
||||||
// | args | X | X | Yes | No | Optional |
|
|
||||||
// | prf | X | X | Yes | No | Optional |
|
|
||||||
// | iat | X | X | No | N/A | Optional |
|
|
||||||
// | cause | X | X | No | N/A | Optional |
|
|
||||||
//
|
|
||||||
// [TokenPayload]: https://github.com/ucan-wg/spec?tab=readme-ov-file#envelope
|
|
||||||
package token
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package token
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var ErrFailedSchemaLoad = errors.New("failed to load IPLD Schema")
|
|
||||||
|
|
||||||
var ErrNoSchemaType = errors.New("schema does not contain type")
|
|
||||||
|
|
||||||
var ErrNodeNotToken = errors.New("IPLD node is not a Token")
|
|
||||||
|
|
||||||
var ErrMissingRequiredDID = errors.New("a required DID is missing")
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package token
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
|
||||||
)
|
|
||||||
|
|
||||||
const tokenTypeName = "Token"
|
|
||||||
|
|
||||||
//go:embed token.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("%w: %w", ErrFailedSchemaLoad, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
tknType := ts.TypeByName(tokenTypeName)
|
|
||||||
if tknType == nil {
|
|
||||||
panic(fmt.Errorf("%w: %s", ErrNoSchemaType, tokenTypeName))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ts
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenType() schema.Type {
|
|
||||||
return mustLoadSchema().TypeByName(tokenTypeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Prototype() schema.TypedPrototype {
|
|
||||||
return bindnode.Prototype((*Token)(nil), tokenType())
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package token_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/token"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed token.ipldsch
|
|
||||||
var schemaBytes []byte
|
|
||||||
|
|
||||||
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 --> IPLD node --> token --> dagCbor --> IPLD node --> dagJson
|
|
||||||
// function: Unwrap() Wrap()
|
|
||||||
|
|
||||||
n1, err := ipld.DecodeUsingPrototype([]byte(delegationJson), dagjson.Decode, token.Prototype())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cborBytes, err := ipld.Encode(n1, dagcbor.Encode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("cborBytes length", len(cborBytes))
|
|
||||||
fmt.Println("cbor", string(cborBytes))
|
|
||||||
|
|
||||||
n2, err := ipld.DecodeUsingPrototype(cborBytes, dagcbor.Decode, token.Prototype())
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("read Cbor", n2)
|
|
||||||
|
|
||||||
t1, err := token.Unwrap(n2)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
n3 := t1.Wrap()
|
|
||||||
|
|
||||||
readJson, err := ipld.Encode(n3, dagjson.Encode)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package token
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:generate go run ../cmd/token/...
|
|
||||||
|
|
||||||
// Unwrap creates a Token from an arbitrary IPLD node or returns an
|
|
||||||
// error if at least the required model fields are not present.
|
|
||||||
//
|
|
||||||
// It is the responsibility of the Delegation and Invocation views
|
|
||||||
// to further validate the presence of the required fields and the
|
|
||||||
// content as needed.
|
|
||||||
func Unwrap(node datamodel.Node) (*Token, error) {
|
|
||||||
iface := bindnode.Unwrap(node)
|
|
||||||
if iface == nil {
|
|
||||||
return nil, ErrNodeNotToken
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn, ok := iface.(*Token)
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNodeNotToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap creates an IPLD node representing the Token.
|
|
||||||
func (t *Token) Wrap() datamodel.Node {
|
|
||||||
return bindnode.Wrap(t, tokenType())
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
|
|
||||||
type CID string
|
|
||||||
|
|
||||||
type Command string
|
|
||||||
|
|
||||||
type DID string
|
|
||||||
|
|
||||||
# Field requirements:
|
|
||||||
#
|
|
||||||
# | Name | Delegation | Invocation | Token |
|
|
||||||
# | | Required | Nullable | Required | Nullable | |
|
|
||||||
# | ----- | -------- | -------- | -------- | -------- | -------- |
|
|
||||||
# | iss | Yes | No | Yes | No | |
|
|
||||||
# | aud | Yes | No | No | N/A | Optional |
|
|
||||||
# | sub | Yes | Yes | Yes | No | Nullable |
|
|
||||||
# | cmd | Yes | No | Yes | No | |
|
|
||||||
# | pol | Yes | No | X | X | Optional |
|
|
||||||
# | nonce | Yes | No | No | N/A | Optional |
|
|
||||||
# | meta | No | N/A | No | N/A | Optional |
|
|
||||||
# | nbf | No | N/A | X | X | Optional |
|
|
||||||
# | exp | Yes | Yes | Yes | Yes | Nullable |
|
|
||||||
# | args | X | X | Yes | No | Optional |
|
|
||||||
# | prf | X | X | Yes | No | Optional |
|
|
||||||
# | iat | X | X | No | N/A | Optional |
|
|
||||||
# | cause | X | X | No | N/A | Optional |
|
|
||||||
|
|
||||||
type Token struct {
|
|
||||||
# Issuer DID (sender)
|
|
||||||
issuer DID (rename "iss")
|
|
||||||
# Audience DID (receiver)
|
|
||||||
audience optional DID (rename "aud")
|
|
||||||
# Principal that the chain is about (the Subject)
|
|
||||||
subject nullable DID (rename "sub")
|
|
||||||
|
|
||||||
# The Command to eventually invoke
|
|
||||||
command Command (rename "cmd")
|
|
||||||
|
|
||||||
# The delegation policy
|
|
||||||
# It doesn't seem possible to represent it with a schema.
|
|
||||||
policy optional Any (rename "pol")
|
|
||||||
|
|
||||||
# The invocation's arguments
|
|
||||||
arguments optional {String: Any} (rename "args")
|
|
||||||
|
|
||||||
# Delegations that prove the chain of authority
|
|
||||||
Proofs optional [CID] (rename "prf")
|
|
||||||
|
|
||||||
# A unique, random nonce
|
|
||||||
nonce optional Bytes
|
|
||||||
|
|
||||||
# Arbitrary Metadata
|
|
||||||
meta optional {String : Any}
|
|
||||||
|
|
||||||
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
|
||||||
notBefore optional Int (rename "nbf")
|
|
||||||
# The timestamp at which the delegation becomes invalid
|
|
||||||
expiration nullable Int (rename "exp")
|
|
||||||
# The timestamp at which the invocation was created
|
|
||||||
issuedAt optional Int
|
|
||||||
|
|
||||||
# An optional CID of the receipt that enqueued this invocation
|
|
||||||
cause optional CID
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// Code generated by internal/cmd/token - DO NOT EDIT.
|
|
||||||
|
|
||||||
package token
|
|
||||||
|
|
||||||
import "github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
|
|
||||||
type Map struct {
|
|
||||||
Keys []string
|
|
||||||
Values map[string]datamodel.Node
|
|
||||||
}
|
|
||||||
type List []datamodel.Node
|
|
||||||
type Map__String__Any struct {
|
|
||||||
Keys []string
|
|
||||||
Values map[string]datamodel.Node
|
|
||||||
}
|
|
||||||
type List__CID []string
|
|
||||||
type Token struct {
|
|
||||||
Issuer string
|
|
||||||
Audience *string
|
|
||||||
Subject *string
|
|
||||||
Command string
|
|
||||||
Policy *datamodel.Node
|
|
||||||
Arguments *Map__String__Any
|
|
||||||
Proofs *List__CID
|
|
||||||
Nonce *[]uint8
|
|
||||||
Meta *Map__String__Any
|
|
||||||
NotBefore *int
|
|
||||||
Expiration *int
|
|
||||||
IssuedAt *int
|
|
||||||
Cause *string
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package token_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/token"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEncode(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tkn := &token.Token{}
|
|
||||||
|
|
||||||
node := tkn.Wrap()
|
|
||||||
|
|
||||||
json, err := ipld.Encode(node, dagjson.Encode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Log(string(json))
|
|
||||||
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrototype(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tkn := &token.Token{
|
|
||||||
Issuer: "blah",
|
|
||||||
}
|
|
||||||
n1 := tkn.Wrap()
|
|
||||||
json, err := ipld.Encode(n1, dagjson.Encode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Log(string(json))
|
|
||||||
|
|
||||||
n2, err := ipld.Decode(json, dagjson.Decode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
nb := token.Prototype().Representation().NewBuilder()
|
|
||||||
require.NoError(t, nb.AssignNode(n2))
|
|
||||||
|
|
||||||
n3 := nb.Build()
|
|
||||||
|
|
||||||
tkn2, err := token.Unwrap(n3)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Log(tkn2)
|
|
||||||
|
|
||||||
require.Equal(t, tkn, tkn2)
|
|
||||||
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build tools
|
|
||||||
|
|
||||||
package tools
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "github.com/selesy/go-options"
|
|
||||||
)
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// Package varsig implements the portion of the [varsig specification]
|
|
||||||
// that's needed for the UCAN [Envelope].
|
|
||||||
//
|
|
||||||
// While the [Envelope] specification has a field that's labelled
|
|
||||||
// "VarsigHeader", this field is actually the prefix, header and segments
|
|
||||||
// of the body excluding the signature itself (which is a different field
|
|
||||||
// in the [Envelope]).
|
|
||||||
//
|
|
||||||
// Given that [go-ucan] supports a limited number of public key types,
|
|
||||||
// and that the signature isn't part of the resulting field, the values
|
|
||||||
// that are used are constants. Note that for key types that are fully
|
|
||||||
// specified in the [did:key], the [VarsigHeader] field isn't technically
|
|
||||||
// needed and could theoretically conflict with the DID.
|
|
||||||
//
|
|
||||||
// Treating these values as constants has no impact when issuing or
|
|
||||||
// delegating tokens. When decoding tokens, simply matching the strings
|
|
||||||
// will allow us to detect errors but won't provide as much detail (e.g.
|
|
||||||
// we can't indicate that the signature was incorrectly generated from
|
|
||||||
// a DAG-JSON encoding.)
|
|
||||||
//
|
|
||||||
// [varsig specification]: https://github.com/ChainAgnostic/varsig
|
|
||||||
// [Envelope]:https://github.com/ucan-wg/spec#envelope
|
|
||||||
// [go-ucan]: https://github.com/ucan-wg/go-ucan
|
|
||||||
package varsig
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
|
||||||
"github.com/multiformats/go-multicodec"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Prefix = 0x34
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrUnknownHeader is returned when it's not possible to decode the
|
|
||||||
// provided string into a libp2p public key type.
|
|
||||||
var ErrUnknownHeader = errors.New("could not decode unknown header")
|
|
||||||
|
|
||||||
// ErrUnknownKeyType is returned when value provided is not a valid
|
|
||||||
// libp2p public key type.
|
|
||||||
var ErrUnknownKeyType = errors.New("could not encode unsupported key type")
|
|
||||||
|
|
||||||
var (
|
|
||||||
decMap = headerToKeyType()
|
|
||||||
encMap = keyTypeToHeader()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Decode returns either the pb.KeyType associated with the provided Header
|
|
||||||
// or an error.
|
|
||||||
//
|
|
||||||
// Currently, only the four key types supported by the [go-libp2p/core/crypto]
|
|
||||||
// library are supported.
|
|
||||||
//
|
|
||||||
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
|
|
||||||
func Decode(header []byte) (pb.KeyType, error) {
|
|
||||||
keyType, ok := decMap[base64.RawStdEncoding.EncodeToString(header)]
|
|
||||||
if !ok {
|
|
||||||
return -1, fmt.Errorf("%w: %s", ErrUnknownHeader, header)
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode returns either the header associated with the provided pb.KeyType
|
|
||||||
// or an error indicating the header was unknown.
|
|
||||||
//
|
|
||||||
// Currently, only the four key types supported by the [go-libp2p/core/crypto]
|
|
||||||
// library are supported.
|
|
||||||
//
|
|
||||||
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
|
|
||||||
func Encode(keyType pb.KeyType) ([]byte, error) {
|
|
||||||
header, ok := encMap[keyType]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrUnknownKeyType, keyType.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(header), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyTypeToHeader() map[pb.KeyType][]byte {
|
|
||||||
const rsaSigLen = 0x100
|
|
||||||
|
|
||||||
return map[pb.KeyType][]byte{
|
|
||||||
pb.KeyType_RSA: header(
|
|
||||||
Prefix,
|
|
||||||
multicodec.RsaPub,
|
|
||||||
multicodec.Sha2_256,
|
|
||||||
rsaSigLen,
|
|
||||||
multicodec.DagCbor,
|
|
||||||
),
|
|
||||||
pb.KeyType_Ed25519: header(
|
|
||||||
Prefix,
|
|
||||||
multicodec.Ed25519Pub,
|
|
||||||
multicodec.DagCbor,
|
|
||||||
),
|
|
||||||
pb.KeyType_Secp256k1: header(
|
|
||||||
Prefix,
|
|
||||||
multicodec.Secp256k1Pub,
|
|
||||||
multicodec.Sha2_256,
|
|
||||||
multicodec.DagCbor,
|
|
||||||
),
|
|
||||||
pb.KeyType_ECDSA: header(
|
|
||||||
Prefix,
|
|
||||||
multicodec.Es256,
|
|
||||||
multicodec.Sha2_256,
|
|
||||||
multicodec.DagCbor,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerToKeyType() map[string]pb.KeyType {
|
|
||||||
out := make(map[string]pb.KeyType, len(encMap))
|
|
||||||
|
|
||||||
for keyType, header := range encMap {
|
|
||||||
out[base64.RawStdEncoding.EncodeToString(header)] = keyType
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func header(vals ...multicodec.Code) []byte {
|
|
||||||
var buf []byte
|
|
||||||
|
|
||||||
for _, val := range vals {
|
|
||||||
buf = binary.AppendUvarint(buf, uint64(val))
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package varsig_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/internal/varsig"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDecode(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
notAHeader := base64.RawStdEncoding.EncodeToString([]byte("not a header"))
|
|
||||||
keyType, err := varsig.Decode([]byte(notAHeader))
|
|
||||||
assert.Equal(t, pb.KeyType(-1), keyType)
|
|
||||||
assert.ErrorIs(t, err, varsig.ErrUnknownHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleDecode() {
|
|
||||||
hdr, err := base64.RawStdEncoding.DecodeString("NIUkEoACcQ")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
keyType, _ := varsig.Decode(hdr)
|
|
||||||
fmt.Println(keyType.String())
|
|
||||||
// Output:
|
|
||||||
// RSA
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncode(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
header, err := varsig.Encode(pb.KeyType(99))
|
|
||||||
assert.Nil(t, header)
|
|
||||||
assert.ErrorIs(t, err, varsig.ErrUnknownKeyType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleEncode() {
|
|
||||||
header, _ := varsig.Encode(pb.KeyType_RSA)
|
|
||||||
fmt.Println(base64.RawStdEncoding.EncodeToString(header))
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// NIUkEoACcQ
|
|
||||||
}
|
|
||||||
187
pkg/args/args.go
Normal file
187
pkg/args/args.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
"github.com/ucan-wg/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
|
||||||
|
}
|
||||||
297
pkg/args/args_test.go
Normal file
297
pkg/args/args_test.go
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
"github.com/ucan-wg/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
|
||||||
|
}
|
||||||
71
pkg/args/builder.go
Normal file
71
pkg/args/builder.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
82
pkg/args/builder_test.go
Normal file
82
pkg/args/builder_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package args_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/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)
|
||||||
|
})
|
||||||
|
}
|
||||||
39
pkg/args/readonly.go
Normal file
39
pkg/args/readonly.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
265
pkg/claims/claims.go
Normal file
265
pkg/claims/claims.go
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
package claims
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/printer"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/secretbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("key not found in claims")
|
||||||
|
|
||||||
|
var ErrNotEncryptable = errors.New("value of this type cannot be encrypted")
|
||||||
|
|
||||||
|
// Claims is a container for claims key-value pairs in an attestation 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 Claims struct {
|
||||||
|
// This type must be compatible with the IPLD type represented by the IPLD
|
||||||
|
// schema { String : Any }.
|
||||||
|
|
||||||
|
Keys []string
|
||||||
|
Values map[string]ipld.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClaims constructs a new Claims.
|
||||||
|
func NewClaims() *Claims {
|
||||||
|
return &Claims{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 *Claims) 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 *Claims) 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 *Claims) 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 *Claims) 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 *Claims) 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 *Claims) 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 *Claims) 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 *Claims) 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 claims set.
|
||||||
|
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
||||||
|
func (m *Claims) 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 claims 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 *Claims) 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 claims into the existing one.
|
||||||
|
//
|
||||||
|
// If duplicate keys are encountered, the new value is silently dropped
|
||||||
|
// without causing an error.
|
||||||
|
func (m *Claims) 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 *Claims) Len() int {
|
||||||
|
return len(m.Values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iter iterates over the claims key/values
|
||||||
|
func (m *Claims) 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 Claims hold the same key/values.
|
||||||
|
func (m *Claims) Equals(other *Claims) 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 *Claims) 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 Claims.
|
||||||
|
func (m *Claims) ReadOnly() ReadOnly {
|
||||||
|
return ReadOnly{claims: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone makes a deep copy.
|
||||||
|
func (m *Claims) Clone() *Claims {
|
||||||
|
res := &Claims{
|
||||||
|
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
|
||||||
|
}
|
||||||
130
pkg/claims/claims_test.go
Normal file
130
pkg/claims/claims_test.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package claims_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"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/claims"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClaims_Add(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type Unsupported struct{}
|
||||||
|
|
||||||
|
t.Run("error if not primitive or Node", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := (&claims.Claims{}).Add("invalid", &Unsupported{})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("encrypted claims", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := make([]byte, 32)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m := claims.NewClaims()
|
||||||
|
|
||||||
|
// 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, claims.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 := claims.NewClaims()
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
// claims -> iter
|
||||||
|
require.Equal(t, expected, maps.Collect(m.Iter()))
|
||||||
|
|
||||||
|
// readonly -> iter
|
||||||
|
ro := m.ReadOnly()
|
||||||
|
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
||||||
|
|
||||||
|
// claims -> 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 := claims.NewClaims()
|
||||||
|
|
||||||
|
require.NoError(t, m1.Add("samekey", "bar"))
|
||||||
|
require.NoError(t, m1.Add("baz", 1234))
|
||||||
|
|
||||||
|
m2 := claims.NewClaims()
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
64
pkg/claims/readonly.go
Normal file
64
pkg/claims/readonly.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package claims
|
||||||
|
|
||||||
|
import (
|
||||||
|
"iter"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadOnly wraps a Claims into a read-only facade.
|
||||||
|
type ReadOnly struct {
|
||||||
|
claims *Claims
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetBool(key string) (bool, error) {
|
||||||
|
return r.claims.GetBool(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetString(key string) (string, error) {
|
||||||
|
return r.claims.GetString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
||||||
|
return r.claims.GetEncryptedString(key, encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
||||||
|
return r.claims.GetInt64(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetFloat64(key string) (float64, error) {
|
||||||
|
return r.claims.GetFloat64(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetBytes(key string) ([]byte, error) {
|
||||||
|
return r.claims.GetBytes(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
||||||
|
return r.claims.GetEncryptedBytes(key, encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||||
|
return r.claims.GetNode(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Len() int {
|
||||||
|
return r.claims.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
||||||
|
return r.claims.Iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Equals(other ReadOnly) bool {
|
||||||
|
return r.claims.Equals(other.claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) String() string {
|
||||||
|
return r.claims.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) WriteableClone() *Claims {
|
||||||
|
return r.claims.Clone()
|
||||||
|
}
|
||||||
@@ -16,15 +16,12 @@ var _ fmt.Stringer = (*Command)(nil)
|
|||||||
// by one or more slash-separated Segments of lowercase characters.
|
// by one or more slash-separated Segments of lowercase characters.
|
||||||
//
|
//
|
||||||
// [Command]: https://github.com/ucan-wg/spec#command
|
// [Command]: https://github.com/ucan-wg/spec#command
|
||||||
type Command struct {
|
type Command string
|
||||||
segments []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a validated command from the provided list of segment
|
// New creates a validated command from the provided list of segment strings.
|
||||||
// strings. An error is returned if an invalid Command would be
|
// An error is returned if an invalid Command would be formed
|
||||||
// formed
|
func New(segments ...string) Command {
|
||||||
func New(segments ...string) *Command {
|
return Top().Join(segments...)
|
||||||
return &Command{segments: segments}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse verifies that the provided string contains the required
|
// Parse verifies that the provided string contains the required
|
||||||
@@ -32,26 +29,26 @@ func New(segments ...string) *Command {
|
|||||||
// Command.
|
// Command.
|
||||||
//
|
//
|
||||||
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
func Parse(s string) (*Command, error) {
|
func Parse(s string) (Command, error) {
|
||||||
if !strings.HasPrefix(s, "/") {
|
if !strings.HasPrefix(s, "/") {
|
||||||
return nil, ErrRequiresLeadingSlash
|
return "", ErrRequiresLeadingSlash
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
||||||
return nil, ErrDisallowsTrailingSlash
|
return "", ErrDisallowsTrailingSlash
|
||||||
}
|
}
|
||||||
|
|
||||||
if s != strings.ToLower(s) {
|
if s != strings.ToLower(s) {
|
||||||
return nil, ErrRequiresLowercase
|
return "", ErrRequiresLowercase
|
||||||
}
|
}
|
||||||
|
|
||||||
// The leading slash will result in the first element from strings.Split
|
// 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.
|
// being an empty string which is removed as strings.Join will ignore it.
|
||||||
return &Command{strings.Split(s, "/")[1:]}, nil
|
return Command(s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustParse is the same as Parse, but panic() if the parsing fail.
|
// MustParse is the same as Parse, but panic() if the parsing fail.
|
||||||
func MustParse(s string) *Command {
|
func MustParse(s string) Command {
|
||||||
c, err := Parse(s)
|
c, err := Parse(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -59,15 +56,14 @@ func MustParse(s string) *Command {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Top] is the most powerful capability.
|
// Top is the most powerful capability.
|
||||||
//
|
//
|
||||||
// This function returns a Command that is a wildcard and therefore represents the
|
// 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
|
// most powerful ability. As such, it should be handled with care and used sparingly.
|
||||||
// sparingly.
|
|
||||||
//
|
//
|
||||||
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
||||||
func Top() *Command {
|
func Top() Command {
|
||||||
return New()
|
return Command(separator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns true if the provided string is a valid UCAN command.
|
// IsValid returns true if the provided string is a valid UCAN command.
|
||||||
@@ -78,18 +74,62 @@ func IsValid(s string) bool {
|
|||||||
|
|
||||||
// Join appends segments to the end of this command using the required
|
// Join appends segments to the end of this command using the required
|
||||||
// segment separator.
|
// segment separator.
|
||||||
func (c *Command) Join(segments ...string) *Command {
|
func (c Command) Join(segments ...string) Command {
|
||||||
return &Command{append(c.segments, segments...)}
|
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
|
// Segments returns the ordered segments that comprise the Command as a
|
||||||
// slice of strings.
|
// slice of strings.
|
||||||
func (c *Command) Segments() []string {
|
func (c Command) Segments() []string {
|
||||||
return c.segments
|
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
|
// String returns the composed representation the command. This is also
|
||||||
// the required wire representation (before IPLD encoding occurs.)
|
// the required wire representation (before IPLD encoding occurs.)
|
||||||
func (c *Command) String() string {
|
func (c Command) String() string {
|
||||||
return "/" + strings.Join(c.segments, "/")
|
return string(c)
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/capability/command"
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTop(t *testing.T) {
|
func TestTop(t *testing.T) {
|
||||||
@@ -13,73 +13,81 @@ 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.NotNil(t, cmd)
|
require.NotEmpty(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, testcase.err)
|
require.ErrorIs(t, err, testcase.err)
|
||||||
require.Nil(t, cmd)
|
require.Zero(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
|
||||||
88
pkg/container/Readme.md
Normal file
88
pkg/container/Readme.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# 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.**
|
||||||
263
pkg/container/car.go
Normal file
263
pkg/container/car.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
85
pkg/container/car_test.go
Normal file
85
pkg/container/car_test.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
1
pkg/container/containertest/Base64StdPadding
Normal file
1
pkg/container/containertest/Base64StdPadding
Normal file
File diff suppressed because one or more lines are too long
1
pkg/container/containertest/Base64StdPaddingGzipped
Normal file
1
pkg/container/containertest/Base64StdPaddingGzipped
Normal file
@@ -0,0 +1 @@
|
|||||||
|
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=
|
||||||
1
pkg/container/containertest/Base64URL
Normal file
1
pkg/container/containertest/Base64URL
Normal file
File diff suppressed because one or more lines are too long
1
pkg/container/containertest/Base64URLGzipped
Normal file
1
pkg/container/containertest/Base64URLGzipped
Normal file
@@ -0,0 +1 @@
|
|||||||
|
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
|
||||||
BIN
pkg/container/containertest/Bytes
Normal file
BIN
pkg/container/containertest/Bytes
Normal file
Binary file not shown.
BIN
pkg/container/containertest/BytesGzipped
Normal file
BIN
pkg/container/containertest/BytesGzipped
Normal file
Binary file not shown.
21
pkg/container/containertest/containertest.go
Normal file
21
pkg/container/containertest/containertest.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
BIN
pkg/container/img/alloc_byte.png
Normal file
BIN
pkg/container/img/alloc_byte.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
pkg/container/img/alloc_count.png
Normal file
BIN
pkg/container/img/alloc_count.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
pkg/container/img/cpu.png
Normal file
BIN
pkg/container/img/cpu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
pkg/container/img/overhead_bytes.png
Normal file
BIN
pkg/container/img/overhead_bytes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
pkg/container/img/overhead_percent.png
Normal file
BIN
pkg/container/img/overhead_percent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
118
pkg/container/packaging.go
Normal file
118
pkg/container/packaging.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
235
pkg/container/reader.go
Normal file
235
pkg/container/reader.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/token"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
|
"github.com/ucan-wg/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
|
||||||
|
}
|
||||||
243
pkg/container/serial_test.go
Normal file
243
pkg/container/serial_test.go
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/MetaMask/go-did-it"
|
||||||
|
"github.com/MetaMask/go-did-it/controller/did-key"
|
||||||
|
"github.com/MetaMask/go-did-it/crypto/ed25519"
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
"github.com/ucan-wg/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
Normal file
BIN
pkg/container/testdata/sample-v1.car
vendored
Normal file
Binary file not shown.
145
pkg/container/writer.go
Normal file
145
pkg/container/writer.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
18
pkg/container/writer_test.go
Normal file
18
pkg/container/writer_test.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
Normal file
265
pkg/meta/meta.go
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
package meta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/printer"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
"github.com/ucan-wg/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
|
||||||
|
}
|
||||||
130
pkg/meta/meta_test.go
Normal file
130
pkg/meta/meta_test.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/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()))
|
||||||
|
}
|
||||||
64
pkg/meta/readonly.go
Normal file
64
pkg/meta/readonly.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
79
pkg/policy/glob.go
Normal file
79
pkg/policy/glob.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
73
pkg/policy/glob_test.go
Normal file
73
pkg/policy/glob_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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!")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,15 @@ 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"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
"github.com/ucan-wg/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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +66,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return Not(statement), nil
|
return negation{statement: statement}, nil
|
||||||
|
|
||||||
case KindAnd, KindOr:
|
case KindAnd, KindOr:
|
||||||
arg2, _ := node.LookupByIndex(1)
|
arg2, _ := node.LookupByIndex(1)
|
||||||
@@ -76,7 +81,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
}
|
}
|
||||||
case 3:
|
case 3:
|
||||||
switch op {
|
switch op {
|
||||||
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
case KindEqual, KindNotEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
||||||
sel, err := arg2AsSelector(op)
|
sel, err := arg2AsSelector(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -93,11 +98,11 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
if pattern.Kind() != datamodel.Kind_String {
|
if pattern.Kind() != datamodel.Kind_String {
|
||||||
return nil, ErrNotAString(combinePath(path, op, 2))
|
return nil, ErrNotAString(combinePath(path, op, 2))
|
||||||
}
|
}
|
||||||
res, err := Like(sel, must.String(pattern))
|
g, err := parseGlob(must.String(pattern))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
|
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
|
||||||
}
|
}
|
||||||
return res, nil
|
return wildcard{selector: sel, pattern: g}, nil
|
||||||
|
|
||||||
case KindAll, KindAny:
|
case KindAll, KindAny:
|
||||||
sel, err := arg2AsSelector(op)
|
sel, err := arg2AsSelector(op)
|
||||||
@@ -232,7 +237,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = listBuilder.AssembleValue().AssignString(statement.pattern)
|
err = listBuilder.AssembleValue().AssignString(string(statement.pattern))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -16,13 +16,36 @@ func TestIpldRoundTrip(t *testing.T) {
|
|||||||
["any", ".tags",
|
["any", ".tags",
|
||||||
["or", [
|
["or", [
|
||||||
["==", ".", "news"],
|
["==", ".", "news"],
|
||||||
["==", ".", "press"]]]
|
["==", ".", "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)
|
||||||
49
pkg/policy/limits/int.go
Normal file
49
pkg/policy/limits/int.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
82
pkg/policy/limits/int_test.go
Normal file
82
pkg/policy/limits/int_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
205
pkg/policy/literal/literal.go
Normal file
205
pkg/policy/literal/literal.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/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))
|
||||||
|
}
|
||||||
314
pkg/policy/literal/literal_test.go
Normal file
314
pkg/policy/literal/literal_test.go
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/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
|
||||||
|
}
|
||||||
306
pkg/policy/match.go
Normal file
306
pkg/policy/match.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
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 }
|
||||||
1071
pkg/policy/match_test.go
Normal file
1071
pkg/policy/match_test.go
Normal file
File diff suppressed because it is too large
Load Diff
254
pkg/policy/policy.go
Normal file
254
pkg/policy/policy.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
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 "github.com/ucan-wg/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
|
||||||
|
}
|
||||||
95
pkg/policy/policy_test.go
Normal file
95
pkg/policy/policy_test.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package policy_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/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)
|
||||||
|
}
|
||||||
67
pkg/policy/policytest/spec.go
Normal file
67
pkg/policy/policytest/spec.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package policytest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/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
|
||||||
|
}
|
||||||
32
pkg/policy/policytest/spec_test.go
Normal file
32
pkg/policy/policytest/spec_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
209
pkg/policy/selector/parsing.go
Normal file
209
pkg/policy/selector/parsing.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/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}
|
||||||
|
}
|
||||||
641
pkg/policy/selector/parsing_test.go
Normal file
641
pkg/policy/selector/parsing_test.go
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/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)
|
||||||
|
})
|
||||||
|
}
|
||||||
342
pkg/policy/selector/selector.go
Normal file
342
pkg/policy/selector/selector.go
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
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}
|
||||||
|
}
|
||||||
413
pkg/policy/selector/selector_test.go
Normal file
413
pkg/policy/selector/selector_test.go
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
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,18 +1,15 @@
|
|||||||
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"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSupported Forms runs tests against the Selector according to the
|
// TestSupported Forms runs tests against the Selector according to the
|
||||||
@@ -26,18 +23,16 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
Output string
|
Output string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass
|
// Pass and return a node
|
||||||
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: "String Index", Selector: `.[0]`, Input: `"Hi"`, Output: `"H"`},
|
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}}`, Output: `0`},
|
||||||
{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]`},
|
||||||
@@ -52,35 +47,16 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
res, err := sel.Select(makeNode(t, tc.Input))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set)
|
require.NotNil(t, res)
|
||||||
|
|
||||||
// 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)
|
||||||
equalIPLD(t, exp, node)
|
require.True(t, ipld.DeepEqual(exp, res))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// null
|
// No error and return null, as optional
|
||||||
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`},
|
||||||
@@ -97,19 +73,15 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
res, err := sel.Select(makeNode(t, tc.Input))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// TODO: should Select return a single node which is sometimes a list or null?
|
require.Nil(t, res)
|
||||||
// require.Equal(t, datamodel.Null, node)
|
|
||||||
assert.Nil(t, node)
|
|
||||||
assert.Empty(t, nodes)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// error
|
// fail to select and return an 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: `[]`},
|
||||||
@@ -124,31 +96,13 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
res, err := sel.Select(makeNode(t, tc.Input))
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Nil(t, node)
|
require.Nil(t, res)
|
||||||
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()
|
||||||
|
|
||||||
90
pkg/secretbox/secretbox.go
Normal file
90
pkg/secretbox/secretbox.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
144
pkg/secretbox/secretbox_test.go
Normal file
144
pkg/secretbox/secretbox_test.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user