Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The following emojis are used to highlight certain changes:

### Added

- ✨ `ipld/unixfs`: reads of both `PBNode` field orders are now covered by tests, and a documented low-level opt-in (`UnixFSProfile.PBNodeFieldOrder`, applied via `merkledag.DefaultPBNodeFieldOrder`) lets writers that need streaming-friendly blocks encode the `Data` field before `Links` per [IPIP-550](https://github.com/ipfs/specs/pull/550). Off by default and selected by no named profile: `UnixFS_v0_2015` and `UnixFS_v1_2025` pin the canonical links-first order explicitly, so defaults and existing CIDs are unchanged. Enabling data-first changes the CID of every dag-pb node that has both fields (directories, HAMT shards, multi-chunk file roots), is process-wide (`ApplyGlobals` affects every `merkledag.ProtoNode` encoded in the process, not only UnixFS nodes), and re-encodes links-first directories in the new order the next time they are opened through the directory API and stored again (for example MFS directories on their next access). [#1212](https://github.com/ipfs/boxo/pull/1212)
- ✨ `gateway`: responses now include the `Ipfs-Uri` header with a canonical `ipfs://` or `ipns://` URI for the requested content path, and expose it via the default `Access-Control-Expose-Headers`. The header carries the content root in canonical form (base32 CIDv1 for `/ipfs/`, base36 CIDv1 for cryptographic `/ipns/` names, lowercase FQDN for DNSLink) with percent-encoded path segments, so clients get a value that is safe in HTTP field context regardless of bytes in the underlying path. [IPIP-548](https://github.com/ipfs/specs/pull/548) [#1209](https://github.com/ipfs/boxo/pull/1209)

### Changed
Expand Down
9 changes: 9 additions & 0 deletions ipld/merkledag/coding.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ func (n *ProtoNode) marshalImmutable() (*immutableProtoNode, error) {
if err != nil {
return nil, err
}
switch order := DefaultPBNodeFieldOrder; order {
case PBNodeLinksFirst:
case PBNodeDataFirst:
if n.data != nil {
enc = moveDataFirst(enc, len(n.data))
}
default:
return nil, fmt.Errorf("unknown PBNodeFieldOrder %d", order)
}
return &immutableProtoNode{enc, nd.(dagpb.PBNode)}, nil
}

Expand Down
71 changes: 71 additions & 0 deletions ipld/merkledag/fieldorder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package merkledag

import (
"slices"

"google.golang.org/protobuf/encoding/protowire"
)

// PBNodeFieldOrder selects the order of the top-level PBNode fields in the
// serialized dag-pb form. Both orders decode to the same logical node, but
// produce different bytes and therefore different CIDs.
type PBNodeFieldOrder int

const (
// PBNodeLinksFirst writes the repeated Links field (field number 2)
// before the Data field (field number 1). This is the order the DAG-PB
// spec requires encoders to produce [1], used by all UnixFS profiles
// through unixfs-v1-2025.
//
// [1]: https://ipld.io/specs/codecs/dag-pb/spec/#protobuf-strictness
PBNodeLinksFirst PBNodeFieldOrder = iota

// PBNodeDataFirst writes the Data field (field number 1) before the
// repeated Links field (field number 2), so streaming readers can
// process Data (e.g. HAMT parameters) before reading links. The DAG-PB
// spec says decoders should accept either order [1]; IPIP-550
// (https://github.com/ipfs/specs/pull/550) defines this one as a
// low-level opt-in for writers that need it. No named profile selects
// it, and enabling it changes CIDs.
//
// [1]: https://ipld.io/specs/codecs/dag-pb/spec/#protobuf-strictness
PBNodeDataFirst
)

// DefaultPBNodeFieldOrder is the field order used when encoding a ProtoNode.
// PBNodeDataFirst changes the bytes, and so the CID, of every encoded node
// that has both Data and Links: directories, HAMT shards, and the root and
// intermediate nodes of files larger than one chunk. A node with only one of
// the two fields encodes the same under both orders.
//
// Like the other UnixFS import globals that io.UnixFSProfile.ApplyGlobals
// writes, this is a process-wide setting, not a per-node option.
// Per-node plumbing would touch every producer and consumer of ProtoNode, so
// the global is the accepted compromise. What follows from it:
//
// - Set it once at startup, before the first encode, and never change it
// while the process runs. It is read on every encode without
// synchronization, and a node that was already encoded keeps its cached
// bytes and CID until it is mutated or re-encoded with
// EncodeProtobuf(true).
// - It applies to every ProtoNode, not only UnixFS ones.
// - A node decoded from storage keeps its wire bytes as its encode cache,
// so storing it back unchanged keeps its CID. Copy and every mutation
// drop that cache, and the next encode uses the current order. Switching
// the order on an existing repository therefore changes the CIDs of
// nodes whose content did not change, for example MFS directories,
// which are copied when loaded (io.NewDirectoryFromNode).
var DefaultPBNodeFieldOrder = PBNodeLinksFirst

// moveDataFirst rewrites a links-first dag-pb encoding produced by
// dagpb.AppendEncode into the PBNodeDataFirst order. AppendEncode writes the
// Data field last, so the field occupies the trailing tag+length+bytes span
// of enc and moving that span to the front is a rotation. Reusing the
// reference encoder keeps one source of truth for link sorting and field
// presence. dataLen is the length of the Data field that was encoded; the
// field must be present.
func moveDataFirst(enc []byte, dataLen int) []byte {
span := protowire.SizeTag(1) + protowire.SizeBytes(dataLen)
split := len(enc) - span
return slices.Concat(enc[split:], enc[:split])
}
Loading
Loading