remove in metadata cid_* to put in the cids/ set

This commit is contained in:
Pierre
2026-05-31 11:41:32 +02:00
parent 94ad0c240b
commit f8a10091b8
18 changed files with 1497 additions and 563 deletions
+108 -63
View File
@@ -1,81 +1,126 @@
// Package cid centralizes CID-format helpers: ranking by utility for
// external presentation, normalizing prefixed CID tokens, and the rules for
// picking which CID is the canonical externally-visible identifier of a
// file.
// Package cid centralizes CID-format helpers: decoding a multibase-base32
// CIDv1 into its multicodec parts, ranking by utility for external
// presentation, and picking which CID is the canonical externally-visible
// identifier of a file.
//
// A "CID token" in this codebase is the prefixed form "<algorithm>:<value>",
// e.g. "midhash256:bafkr4ih...", "sha256:bafkrei...", "ipfs:bafy...". The
// prefix makes the type recoverable without parsing the multicodec, which is
// what the reverse index (cid:<token> → uuid), the per-root cids set, and
// the canonical_cid field all rely on.
// Sibling CIDs are stored as a bare-CID key-set (`cids/<cid> = "true"`); a
// CID is self-describing — its algorithm is recoverable from the CIDv1
// codec / multihash code, so there is no `<algorithm>:` prefix and no
// per-algorithm field name. Ranking and selection parse the multicodec
// rather than a string prefix.
package cid
import "strings"
import (
"encoding/base32"
"encoding/binary"
"fmt"
"strings"
)
// Rank returns a score for a CID token. Higher = more preferred as the
// public-facing "canonical" identifier for a file. Unknown algorithms rank
// below all known ones (0) so a plugin that invents a new digest type
// can't accidentally outrank an established one until it's registered here.
// Multicodec / multihash codes used for ranking. Source of truth for the
// digest codes is meta-hash/src/lib/hash-compute/MultiHashData.ts and
// METADATA_KEYS.md §2.
const (
CodecRaw = 0x55 // raw — IPFS content-addressed block
CodecDagPB = 0x70 // dag-pb — IPFS content-addressed block
CodeSha1 = 0x11 // multihash sha1
CodeSha256 = 0x12 // multihash sha2-256
CodeSha3_384 = 0x15 // multihash sha3-384
CodeSha3_256 = 0x16 // multihash sha3-256
CodeMd5 = 0xd5 // multihash md5
CodeCrc32 = 0x0132 // multihash crc32
CodeMidhash256 = 0x1000 // custom — SHA-256(size‖middle 1MB)
CodeBtPiecesRoot = 0xb702 // bittorrent v2 pieces root
CodeBtihV2 = 0x10B7 // custom — BitTorrent v2 info hash (BEP 52)
CodeBtihV1File = 0x1001 // custom — per-file BitTorrent v1 CID
CodeBtihV2File = 0x1002 // custom — per-file BitTorrent v2 CID
)
// Decode parses a multibase-base32 CIDv1 string ("b…") into its content
// codec and multihash function code. It is the inverse of the encoder in
// internal/watcher/midhash.go. Returns an error for anything that isn't a
// CIDv1 in base32lower (e.g. a CIDv0 "Qm…" or a non-CID string).
func Decode(cidStr string) (codec uint64, mhCode uint64, err error) {
if len(cidStr) < 2 || cidStr[0] != 'b' {
return 0, 0, fmt.Errorf("cid %q: not a multibase base32 CIDv1", cidStr)
}
raw, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(cidStr[1:]))
if err != nil {
return 0, 0, fmt.Errorf("cid %q: base32 decode: %w", cidStr, err)
}
version, n := binary.Uvarint(raw)
if n <= 0 {
return 0, 0, fmt.Errorf("cid %q: bad version varint", cidStr)
}
raw = raw[n:]
if version != 1 {
return 0, 0, fmt.Errorf("cid %q: unsupported CID version %d", cidStr, version)
}
codec, n = binary.Uvarint(raw)
if n <= 0 {
return 0, 0, fmt.Errorf("cid %q: bad codec varint", cidStr)
}
raw = raw[n:]
mhCode, n = binary.Uvarint(raw)
if n <= 0 {
return 0, 0, fmt.Errorf("cid %q: bad multihash-code varint", cidStr)
}
return codec, mhCode, nil
}
// Rank returns a score for a bare CID. Higher = more preferred as the
// public-facing "canonical" identifier for a file. Unknown / undecodable
// CIDs rank 0 so a new digest type can't accidentally outrank an
// established one until it's classified here.
//
// We rank by the **multihash code** (the algorithm), with the custom
// BitTorrent CID codecs checked alongside it. This is deliberate: MetaMesh's
// two CID encoders disagree on the *content codec* — meta-hash/the watcher
// set codec == algorithm code, while the fullhash plugin and gateway wrap
// every digest with the raw codec (0x55) and put the algorithm in the
// multihash code. The multihash code is the algorithm in all of them, so it
// is the only reliable discriminator. (The BT info hash is given a distinct
// multihash code — btih-v2 0x10B7 — so it can't be confused with a plain
// sha2-256, see METADATA_KEYS.md §14.13.)
//
// Ordering rationale (utility-weighted, NOT cryptographic-strength):
// - IPFS CIDs win because the network around them — gateways, swarm,
// content-addressing tooling — has the broadest reach. A cryptographically
// equivalent sha256 in a non-CID form has less downstream value.
// - sha256 / sha3 follow: standard, widely recognized, no platform lock-in.
// - BitTorrent infohashes (btih) outrank midhash256 not for strength
// (v1 is sha1, weaker than midhash256's sha256 inner) but for swarm
// interop — a btih makes a file discoverable on existing torrent
// infrastructure that knows nothing about MetaMesh.
// - midhash256 is the floor. Internally meaningful, externally invisible.
func Rank(token string) int {
switch AlgorithmOf(token) {
case "ipfs":
// - dag-pb (chunked IPFS file) wins: best retrieval over the IPFS network.
// - sha2-256 / sha3 digests follow: standard content hashes (a raw IPFS
// block of the file IS its sha2-256, so they share this tier).
// - BitTorrent infohashes (btih v2, per-file v1/v2) outrank midhash256 for
// swarm interop, not strength.
// - midhash256 is the floor among real options: internally meaningful,
// externally invisible.
// - md5 / crc32 / plain sha1 / pieces-root / unknown rank 0.
func Rank(cidStr string) int {
codec, mh, err := Decode(cidStr)
if err != nil {
return 0
}
if codec == CodecDagPB {
return 40
case "sha256", "sha3", "sha3-256", "sha3-384", "sha3-512":
return 30
case "btih", "btih-v2":
}
// BitTorrent: distinct CID codecs (per-file v1/v2) or the btih-v2
// multihash code, in either encoder convention.
if codec == CodeBtihV1File || codec == CodeBtihV2File || codec == CodeBtihV2 ||
mh == CodeBtihV1File || mh == CodeBtihV2File || mh == CodeBtihV2 {
return 20
case "midhash256":
}
switch mh {
case CodeSha256, CodeSha3_256, CodeSha3_384:
return 30
case CodeMidhash256:
return 10
default:
// md5, crc32, plain sha1, pieces-root, unknown.
return 0
}
}
// AlgorithmOf returns the algorithm prefix of a CID token, or "" if the
// token is malformed (no colon, or empty prefix).
func AlgorithmOf(token string) string {
colon := strings.IndexByte(token, ':')
if colon <= 0 {
return ""
}
return token[:colon]
}
// Token assembles a CID token from its algorithm and raw value. Mirror of
// AlgorithmOf — callers writing to the reverse index, cids set, or
// canonical_cid field route through this so the format stays consistent.
func Token(algorithm, raw string) string {
if algorithm == "" || raw == "" {
return ""
}
return algorithm + ":" + raw
}
// ValueOf returns the raw value portion of a CID token (everything after
// the first colon). Returns "" for malformed tokens.
func ValueOf(token string) string {
colon := strings.IndexByte(token, ':')
if colon <= 0 || colon == len(token)-1 {
return ""
}
return token[colon+1:]
}
// Better picks the higher-ranked of two tokens. On ties, returns the
// Better picks the higher-ranked of two CIDs. On ties, returns the
// lexicographically smaller one — gives the reconciler a deterministic
// answer so an unchanged canonical_cid doesn't churn when peers add the
// same digest under a different ordering.
// answer so the chosen canonical doesn't churn when peers add the same
// digest under a different ordering.
func Better(a, b string) string {
ra, rb := Rank(a), Rank(b)
if ra > rb {