Skip to content

Decode container entries in DAG-JSON output - #20

Open
bajtos wants to merge 1 commit into
mainfrom
feat/view-container-json-entries
Open

Decode container entries in DAG-JSON output#20
bajtos wants to merge 1 commit into
mainfrom
feat/view-container-json-entries

Conversation

@bajtos

@bajtos bajtos commented Aug 28, 2026

Copy link
Copy Markdown
Member

Written by Claude.

view --json on a container prints its entries as opaque bytes, so reading anything out of a container takes one view -i N per entry and there is no way to learn the entry count except walking -i until it errors.

The entries are now decoded in place:

$ ucantool view --json container.ucan | jq '.["ctn-v1"] | length'
3

$ ucantool view --json container.ucan | jq -c '.["ctn-v1"][0]'
[{"/":{"bytes":"oqERLgIA2ARG2edPh5/B3vl7AVoTGgHsOW35rsati3wnyeSltaryvuqV4N3iGZ/3VXltF4+iaKSBgLH/avNxDg"}},{"h":{"/":{"bytes":"NAHtAe0BE3E"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:web:ingot.dev.example","cmd":"/s3/request/authorize","exp":null,"iss":"did:web:hilt.dev.example","nonce":{"/":{"bytes":"Su8vYJv5oLeh+xeYd3nqzw"}},"pol":[],"sub":"did:web:hilt.dev.example"}}]

One behaviour change worth review

The entries come out of the input now. They used to come from a re-encode of the decoded container, and that roundtrip sorts entries bytewise and silently drops the ones decodeTokens cannot read, with a nil error. A container holding three entries where one is junk printed two and said nothing, and -j entry N and -i N could be different tokens.

So three things move for a container whose entries were not already sorted, or that holds an entry this tool cannot decode: the --json array, the view table, and the root CID the table prints. Containers this tool produces are already sorted, so nothing moves on the happy path.

An entry that decodes as no known token kind is written as its bytes, {"/":{"bytes":"…"}}. That keeps the entry count and every index aligned with the input, and it makes the array mixed-shape: a caller walking it needs a type check to tell a token from an undecodable entry. The alternative, null, loses the bytes; dropping it is what this change is fixing.

The output is a view rather than a re-encoding. It no longer round-trips back through UnmarshalDagJSON, which took the entries as byte strings.

Duplication to remove later

decodeContainerCBOR strips the codec byte, base64 and gzip, which duplicates about thirty lines that container.Decode keeps unexported. fil-forge/ucantone#55 exports that step as container.DecodeTransport; the copy goes away once it lands and the dependency is bumped. The bump crosses about eight weeks of ucantone history, so it is separate work.

Relation to #19

#19 makes the same change to where the entries come from, because summarising every token needs the same raw-entry walk. Whichever lands first carries it, and the other drops that part on rebase. The features are independent: this one changes what --json prints, #19 adds --summary.

Tests

cmd/view_test.go drives rootCmd the way cmd/delegate_test.go does, building fixtures with ucantool delegate rather than committing any. It pins that every entry of a three-command container is present, that the entries are decoded rather than emitted as bytes, that a container with an undecodable entry reports both entries with the undecodable one as bytes at index 0, that an array index agrees with -i on every entry, and that the audience is reachable without naming the spec version in the path.

`view --json` on a container printed its entries as opaque bytes, taken
from a re-encode of the decoded container. That re-encode sorts entries
bytewise and drops the ones that decode as no known token kind, so both
the entry count and the index `-i` takes could disagree with the input.

The entries are now read out of the input and decoded in place, so
`--json` reports what the file holds:

- `jq '.["ctn-v1"] | length'` counts the tokens in one command
- an array index selects the same token as `--container-index`
- an entry that decodes as no known token kind is written as its bytes
  rather than dropped

Stripping the transport encoding duplicates the codec handling that
`container.Decode` keeps unexported. fil-forge/ucantone#55 exports it,
and the copy goes away once that lands and the dependency is bumped.

Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Comment thread README.md
##### JSON output

The `--json` flag will output `dag-json` encoding of the input.
The `--json` flag outputs the `dag-json` encoding of the input. For a container, the entries are decoded in place and appear in the order they have in the input, so an array index selects the same token as `--container-index`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked Claude to explain this breaking change:

The | jq in the README is cosmetic — the tool prints one line either way, jq just pretty-prints it. Not a change.

The entry shape is a breaking change. .["ctn-v1"][N] was {"/":{"bytes":"…"}} and is now [sig, {tag: payload}]. Verified consequence:

old output → container.UnmarshalDagJSON:
new output → container.UnmarshalDagJSON: expected object open but read [

There's a second problem underneath it. --json is documented as "the dag-json encoding of the input", and for a container the ctn-v1 field genuinely is a list of byte strings. So the new output is valid DAG-JSON but it is not the DAG-JSON encoding of the container. My change quietly redefines what the flag means, and the mixed-shape array (token envelope or raw bytes) is a wart that only exists because of that redefinition.

Worth noting the entry-source fix is separable and not breaking on its own: reading entries from the input instead of the roundtrip fixes the order, the count and the -i disagreement while keeping the byte-string shape.

@bajtos
bajtos requested a review from alanshaw August 28, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant