Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,29 @@ If you have a UCAN container, you can visualize a specific token by index:
ucantool view -i 1 container.ucan
```

##### Summary of every token

The `--summary` flag decodes every token in the input and reports its command, audience and issuer. A single delegation is summarised the same way as a container.

```sh
ucantool view --summary proof.txt
+---+-----------------------+---------------------------+--------------------------+
| # | COMMAND | AUDIENCE | ISSUER |
+---+-----------------------+---------------------------+--------------------------+
| 0 | /s3/request/authorize | did:web:ingot.dev.example | did:web:hilt.dev.example |
+---+-----------------------+---------------------------+--------------------------+
```

Adding `--json` writes the summary as JSON, which also carries the subject, expiration and tag of each token. The keys are fixed by this tool rather than taken from the encoded tag, so a script keeps working when UCAN moves past the current spec version. An entry that decodes as no known token kind reports its index and an error, leaving the other entries readable.

```sh
ucantool view --summary --json proof.txt | jq -r '.[].aud'
did:web:ingot.dev.example
```

##### JSON output

The `--json` flag will output `dag-json` encoding of the input.
Without `--summary`, the `--json` flag outputs the `dag-json` encoding of the input. For a container that means the container itself, with its entries as opaque bytes.

```sh
ucantool view container.bin --json
Expand Down
142 changes: 122 additions & 20 deletions cmd/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package cmd

import (
"bytes"
"compress/gzip"
"encoding/base64"
"errors"
"fmt"
"io"
"os"

"github.com/fil-forge/ucantone/ucan"
"github.com/fil-forge/ucantone/ucan/container"
cdm "github.com/fil-forge/ucantone/ucan/container/datamodel"
"github.com/fil-forge/ucantone/ucan/delegation"
Expand All @@ -21,6 +24,7 @@ var (
// View command flags
containerIndex int
formatJSON bool
summarize bool
)

var viewCmd = &cobra.Command{
Expand All @@ -39,10 +43,15 @@ var viewCmd = &cobra.Command{
func init() {
viewCmd.Flags().IntVarP(&containerIndex, "container-index", "i", -1, "If input is a UCAN container, view the data at this index.")
viewCmd.Flags().BoolVarP(&formatJSON, "json", "j", false, "Format output as DAG-JSON.")
viewCmd.Flags().BoolVar(&summarize, "summary", false, "Report the command, issuer, audience and subject of every token in the input.")
}

// view reads a delegation from a file or stdin and displays its information
func view(cmd *cobra.Command, args []string) error {
if summarize && containerIndex != -1 {
return errors.New("--summary cannot be combined with --container-index")
}

var ucanBytes []byte
// Check if a file path is provided
if len(args) >= 1 {
Expand Down Expand Up @@ -74,25 +83,33 @@ func view(cmd *cobra.Command, args []string) error {
// Try to decode!
ct, err := container.Decode(ucanBytes)
if err == nil {
// encode using raw codec so we can take the hash of the CBOR data
rawContainerBytes, err := container.Encode(container.Raw, ct)
// Read the entries straight out of the input rather than re-encoding the
// decoded container: re-encoding sorts the entries and drops the ones
// that decode as no known token kind, so indices and entry count would
// stop matching the file.
containerBytes, err := decodeContainerCBOR(ucanBytes)
if err != nil {
return fmt.Errorf("encoding raw container bytes: %w", err)
return fmt.Errorf("decoding container bytes: %w", err)
}

model := cdm.ContainerModel{}
if err := model.UnmarshalCBOR(bytes.NewReader(rawContainerBytes[1:])); err != nil {
if err := model.UnmarshalCBOR(bytes.NewReader(containerBytes)); err != nil {
return fmt.Errorf("decoding container model: %w", err)
}

// summarise every token in the container
if summarize {
return printSummary(cmd, model.Ctn1)
}

// view the container
if containerIndex == -1 {
link, err := cid.Prefix{
Version: 1,
Codec: uint64(multicodec.DagCbor),
MhType: uint64(multicodec.Sha2_256),
MhLength: -1,
}.Sum(rawContainerBytes[1:])
}.Sum(containerBytes)
if err != nil {
return fmt.Errorf("hashing data: %w", err)
}
Expand All @@ -112,6 +129,10 @@ func view(cmd *cobra.Command, args []string) error {
ucanBytes = model.Ctn1[containerIndex]
}

if summarize {
return printSummary(cmd, [][]byte{ucanBytes})
}

link, err := cid.V1Builder{
Codec: uint64(multicodec.DagCbor),
MhType: uint64(multicodec.Sha2_256),
Expand All @@ -120,25 +141,106 @@ func view(cmd *cobra.Command, args []string) error {
return fmt.Errorf("hashing data: %w", err)
}

inv, err := invocation.Decode(ucanBytes)
if err == nil {
if formatJSON {
defer cmd.Println()
return inv.MarshalDagJSON(cmd.OutOrStdout())
token, err := decodeToken(ucanBytes)
if err != nil {
return err
}

if formatJSON {
marshaler, ok := token.(interface{ MarshalDagJSON(io.Writer) error })
if !ok {
return errors.New("token cannot be encoded as DAG-JSON")
}
cmd.Println(ucanfmt.FormatInvocationAsTable(link, inv))
return nil
defer cmd.Println()
return marshaler.MarshalDagJSON(cmd.OutOrStdout())
}

dlg, err := delegation.Decode(ucanBytes)
if err == nil {
if formatJSON {
defer cmd.Println()
return dlg.MarshalDagJSON(cmd.OutOrStdout())
switch tok := token.(type) {
case ucan.Invocation:
cmd.Println(ucanfmt.FormatInvocationAsTable(link, tok))
case ucan.Delegation:
cmd.Println(ucanfmt.FormatDelegationAsTable(link, tok))
}
return nil
}

// printSummary decodes every entry and reports its fields. An entry that
// decodes as neither a delegation nor an invocation is reported by index with
// its error, so the remaining entries stay readable.
func printSummary(cmd *cobra.Command, entries [][]byte) error {
summary := make([]ucanfmt.SummaryEntry, 0, len(entries))
for i, entryBytes := range entries {
token, err := decodeToken(entryBytes)
if err != nil {
summary = append(summary, ucanfmt.SummarizeError(i, err))
continue
}
cmd.Println(ucanfmt.FormatDelegationAsTable(link, dlg))
return nil
summary = append(summary, ucanfmt.SummarizeToken(i, token))
}

return errors.New("unable to decode")
if formatJSON {
encoded, err := ucanfmt.FormatSummaryAsJSON(summary)
if err != nil {
return err
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), encoded)
return err
}

_, err := fmt.Fprintln(cmd.OutOrStdout(), ucanfmt.FormatSummaryAsTable(summary))
return err
}

// decodeContainerCBOR strips the container transport encoding and returns the
// CBOR of the container model. It mirrors the codec handling of
// container.Decode, which returns decoded tokens rather than the raw entries.
func decodeContainerCBOR(input []byte) ([]byte, error) {
if len(input) == 0 {
return nil, errors.New("empty container bytes")
}

codec := input[0]
var payload []byte
switch codec {
case container.Raw, container.RawGzip:
payload = input[1:]
case container.Base64, container.Base64Gzip:
decoded, err := base64.StdEncoding.DecodeString(string(input[1:]))
if err != nil {
return nil, fmt.Errorf("decoding base64: %w", err)
}
payload = decoded
case container.Base64url, container.Base64urlGzip:
decoded, err := base64.RawURLEncoding.DecodeString(string(input[1:]))
if err != nil {
return nil, fmt.Errorf("decoding base64url: %w", err)
}
payload = decoded
default:
return nil, fmt.Errorf("unknown codec: 0x%02x", codec)
}

switch codec {
case container.RawGzip, container.Base64Gzip, container.Base64urlGzip:
gz, err := gzip.NewReader(bytes.NewReader(payload))
if err != nil {
return nil, fmt.Errorf("creating gzip reader: %w", err)
}
defer gz.Close()
return io.ReadAll(gz)
}
return payload, nil
}

// decodeToken decodes UCAN bytes as whichever token kind they turn out to be.
// Both the summary and the single-index view go through here, so they cannot
// disagree about what an entry is.
func decodeToken(ucanBytes []byte) (ucan.Token, error) {
if inv, err := invocation.Decode(ucanBytes); err == nil {
return inv, nil
}
if dlg, err := delegation.Decode(ucanBytes); err == nil {
return dlg, nil
}
return nil, errors.New("unable to decode")
}
Loading