Skip to main content

reallyme_codec/
multicodec.rs

1// SPDX-FileCopyrightText: 2026 ReallyMe LLC
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Semantic layer for multicodec lookup operations.
6//!
7//! The public protobuf, FFI, JNI, WASM, and SDK adapters are responsible for
8//! transport validation and representation conversion. This module owns the
9//! operation meaning so adapters do not independently classify multicodec
10//! lookup failures or table metadata.
11
12use codec_multicodec::{
13    lookup_codec_prefix, CodecSpec, CodecTag as PrimitiveCodecTag, KeyLength,
14    KeyMaterialKind as PrimitiveKeyMaterialKind, MULTICODEC_TABLE,
15};
16use std::sync::OnceLock;
17
18const MAX_UNSIGNED_VARINT_BYTES: usize = 9;
19
20static REGISTRY_VALIDITY: OnceLock<Result<(), MulticodecOperationError>> = OnceLock::new();
21
22/// Semantic failure reasons for multicodec lookup operations.
23///
24/// The display strings are fixed and do not include caller input. Boundary
25/// adapters map these reasons into their public typed error contracts.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
27#[non_exhaustive]
28pub enum MulticodecOperationError {
29    /// The requested name is not present in the supported registry.
30    #[error("unknown multicodec name")]
31    UnknownName,
32    /// The input does not start with a supported multicodec prefix.
33    #[error("invalid multicodec prefix")]
34    InvalidPrefix,
35    /// Primitive registry metadata violated an executor invariant.
36    #[error("multicodec registry invariant violation")]
37    RegistryInvariant,
38    /// The bounded semantic result could not reserve its required storage.
39    #[error("multicodec result allocation failed")]
40    AllocationFailure,
41}
42
43/// Semantic class of a supported multicodec entry.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum CodecTag {
47    /// Encryption-scheme identifier.
48    Encryption,
49    /// Hash-function identifier.
50    Hash,
51    /// Raw key-material identifier.
52    Key,
53    /// Multihash identifier.
54    Multihash,
55    /// Multikey-related identifier.
56    Multikey,
57}
58
59/// Semantic key-material classification for a supported multicodec entry.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum KeyMaterialKind {
63    /// The entry does not identify raw key material.
64    NotKey,
65    /// Public key material.
66    PublicKey,
67    /// Private key material.
68    PrivateKey,
69    /// Symmetric key material.
70    SymmetricKey,
71}
72
73/// Semantic length rule for the value described by a multicodec entry.
74///
75/// The primitive registry represents these cases with separate enum variants,
76/// so transport adapters cannot mistake an algorithm identifier for a key.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[non_exhaustive]
79pub enum MulticodecLength {
80    /// Values have the exact byte length carried by the variant.
81    Fixed(usize),
82    /// Valid value lengths vary within limits defined by the owning codec.
83    Variable,
84    /// A value length is not meaningful for this registry entry.
85    NotApplicable,
86}
87
88/// Borrowed semantic metadata for one supported multicodec entry.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct MulticodecSpec<'a> {
91    name: &'a str,
92    tag: CodecTag,
93    key_material: KeyMaterialKind,
94    algorithm_name: &'a str,
95    code: &'a [u8],
96    prefix: &'a [u8],
97    length: MulticodecLength,
98}
99
100/// Semantic result for prefix lookup by payload bytes.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct MulticodecLookup<'a> {
103    name: &'a str,
104    prefix_length: usize,
105    metadata: MulticodecSpec<'a>,
106}
107
108/// Semantic result for the supported multicodec table.
109///
110/// This owner deliberately does not implement `Clone`: duplicating its vector
111/// would allocate outside the fallible, typed construction path.
112#[derive(Debug, PartialEq, Eq)]
113pub struct MulticodecTable<'a> {
114    entries: Vec<MulticodecSpec<'a>>,
115}
116
117impl<'a> MulticodecSpec<'a> {
118    /// Return the canonical registry name.
119    pub const fn name(&self) -> &'a str {
120        self.name
121    }
122
123    /// Return the semantic registry class.
124    pub const fn tag(&self) -> CodecTag {
125        self.tag
126    }
127
128    /// Return the key-material classification.
129    pub const fn key_material(&self) -> KeyMaterialKind {
130        self.key_material
131    }
132
133    /// Return the public algorithm name.
134    pub const fn algorithm_name(&self) -> &'a str {
135        self.algorithm_name
136    }
137
138    /// Return the encoded multicodec varint bytes.
139    pub const fn code(&self) -> &'a [u8] {
140        self.code
141    }
142
143    /// Return the prefix bytes matched or prepended by codec operations.
144    pub const fn prefix(&self) -> &'a [u8] {
145        self.prefix
146    }
147
148    /// Return the semantic length rule for values described by this entry.
149    pub const fn length(&self) -> MulticodecLength {
150        self.length
151    }
152}
153
154impl<'a> MulticodecLookup<'a> {
155    /// Return the canonical name of the matched entry.
156    pub const fn name(&self) -> &'a str {
157        self.name
158    }
159
160    /// Return the number of matched prefix bytes.
161    pub const fn prefix_length(&self) -> usize {
162        self.prefix_length
163    }
164
165    /// Return the complete metadata for the matched entry.
166    pub const fn metadata(&self) -> &MulticodecSpec<'a> {
167        &self.metadata
168    }
169}
170
171impl<'a> MulticodecTable<'a> {
172    /// Return the supported entries in stable registry order.
173    pub fn entries(&self) -> &[MulticodecSpec<'a>] {
174        self.entries.as_slice()
175    }
176}
177
178/// Resolve a canonical multicodec name to semantic registry metadata.
179pub fn prefix_for_name(name: &str) -> Result<MulticodecSpec<'static>, MulticodecOperationError> {
180    validate_registry()?;
181    let Some((canonical_name, spec)) = find_codec_spec(name) else {
182        return Err(MulticodecOperationError::UnknownName);
183    };
184    multicodec_spec(
185        canonical_name,
186        spec.tag,
187        spec.key_material,
188        spec.alg,
189        spec.codec,
190        spec.key_length,
191    )
192}
193
194/// Resolve the known multicodec prefix at the start of `value`.
195pub fn lookup_prefix(value: &[u8]) -> Result<MulticodecLookup<'static>, MulticodecOperationError> {
196    validate_registry()?;
197    let Some(found) = lookup_codec_prefix(value) else {
198        return Err(MulticodecOperationError::InvalidPrefix);
199    };
200    Ok(MulticodecLookup {
201        name: found.name,
202        prefix_length: found.codec.len(),
203        metadata: multicodec_spec(
204            found.name,
205            found.tag,
206            found.key_material,
207            found.alg,
208            found.codec,
209            found.key_length,
210        )?,
211    })
212}
213
214/// Strip a complete, recognized public-key multicodec prefix.
215pub fn strip_prefix(value: &[u8]) -> Result<&[u8], MulticodecOperationError> {
216    validate_registry()?;
217    codec_multicodec::strip_codec_prefix(value).map_err(|_| MulticodecOperationError::InvalidPrefix)
218}
219
220/// Return all supported multicodec entries in stable registry order.
221pub fn supported_table() -> Result<MulticodecTable<'static>, MulticodecOperationError> {
222    validate_registry()?;
223    let mut entries = table_entries_with_capacity(MULTICODEC_TABLE.len())?;
224    for (name, spec) in MULTICODEC_TABLE {
225        entries.push(multicodec_spec(
226            name,
227            spec.tag,
228            spec.key_material,
229            spec.alg,
230            spec.codec,
231            spec.key_length,
232        )?);
233    }
234    Ok(MulticodecTable { entries })
235}
236
237fn table_entries_with_capacity(
238    capacity: usize,
239) -> Result<Vec<MulticodecSpec<'static>>, MulticodecOperationError> {
240    let mut entries = Vec::new();
241    entries
242        .try_reserve(capacity)
243        .map_err(|_| MulticodecOperationError::AllocationFailure)?;
244    Ok(entries)
245}
246
247fn find_codec_spec(codec_name: &str) -> Option<(&'static str, &'static CodecSpec)> {
248    MULTICODEC_TABLE
249        .iter()
250        .find(|(name, _)| *name == codec_name)
251        .map(|(name, spec)| (*name, spec))
252}
253
254fn validate_registry() -> Result<(), MulticodecOperationError> {
255    *REGISTRY_VALIDITY.get_or_init(validate_registry_uncached)
256}
257
258fn validate_registry_uncached() -> Result<(), MulticodecOperationError> {
259    validate_registry_entries(MULTICODEC_TABLE)
260}
261
262fn validate_registry_entries(
263    entries: &[(&str, CodecSpec)],
264) -> Result<(), MulticodecOperationError> {
265    if entries.is_empty() {
266        return Err(MulticodecOperationError::RegistryInvariant);
267    }
268    for (index, (name, spec)) in entries.iter().enumerate() {
269        if name.is_empty() || spec.alg.is_empty() || !is_canonical_u64_varint(spec.codec) {
270            return Err(MulticodecOperationError::RegistryInvariant);
271        }
272
273        let tag = semantic_codec_tag(spec.tag)?;
274        let key_material = semantic_key_material_kind(spec.key_material)?;
275        let is_key_tag = tag == CodecTag::Key;
276        let carries_key_material = key_material != KeyMaterialKind::NotKey;
277        if is_key_tag != carries_key_material {
278            return Err(MulticodecOperationError::RegistryInvariant);
279        }
280        if (is_key_tag && matches!(spec.key_length, KeyLength::NotApplicable))
281            || (!is_key_tag && matches!(spec.key_length, KeyLength::Variable))
282            || matches!(spec.key_length, KeyLength::Fixed(0))
283        {
284            return Err(MulticodecOperationError::RegistryInvariant);
285        }
286
287        let next_index = index
288            .checked_add(1)
289            .ok_or(MulticodecOperationError::RegistryInvariant)?;
290        let remaining = entries
291            .get(next_index..)
292            .ok_or(MulticodecOperationError::RegistryInvariant)?;
293        for (other_name, other_spec) in remaining {
294            let ambiguous_prefix = spec.codec.starts_with(other_spec.codec)
295                || other_spec.codec.starts_with(spec.codec);
296            if name == other_name || ambiguous_prefix {
297                return Err(MulticodecOperationError::RegistryInvariant);
298            }
299        }
300    }
301    Ok(())
302}
303
304fn is_canonical_u64_varint(value: &[u8]) -> bool {
305    if value.len() > MAX_UNSIGNED_VARINT_BYTES {
306        return false;
307    }
308    let Some((&last, leading)) = value.split_last() else {
309        return false;
310    };
311    if last & 0x80 != 0 || leading.iter().any(|byte| byte & 0x80 == 0) {
312        return false;
313    }
314    if !leading.is_empty() && last & 0x7f == 0 {
315        return false;
316    }
317    true
318}
319
320fn multicodec_spec(
321    name: &'static str,
322    tag: PrimitiveCodecTag,
323    key_material: PrimitiveKeyMaterialKind,
324    algorithm_name: &'static str,
325    prefix: &'static [u8],
326    key_length: KeyLength,
327) -> Result<MulticodecSpec<'static>, MulticodecOperationError> {
328    let tag = semantic_codec_tag(tag)?;
329    let key_material = semantic_key_material_kind(key_material)?;
330    let length = match key_length {
331        KeyLength::Fixed(length) => MulticodecLength::Fixed(length),
332        KeyLength::Variable => MulticodecLength::Variable,
333        KeyLength::NotApplicable => MulticodecLength::NotApplicable,
334    };
335    Ok(MulticodecSpec {
336        name,
337        tag,
338        key_material,
339        algorithm_name,
340        code: prefix,
341        prefix,
342        length,
343    })
344}
345
346fn semantic_codec_tag(tag: PrimitiveCodecTag) -> Result<CodecTag, MulticodecOperationError> {
347    match tag {
348        PrimitiveCodecTag::Encryption => Ok(CodecTag::Encryption),
349        PrimitiveCodecTag::Hash => Ok(CodecTag::Hash),
350        PrimitiveCodecTag::Key => Ok(CodecTag::Key),
351        PrimitiveCodecTag::Multihash => Ok(CodecTag::Multihash),
352        PrimitiveCodecTag::Multikey => Ok(CodecTag::Multikey),
353        _ => Err(MulticodecOperationError::RegistryInvariant),
354    }
355}
356
357fn semantic_key_material_kind(
358    kind: PrimitiveKeyMaterialKind,
359) -> Result<KeyMaterialKind, MulticodecOperationError> {
360    match kind {
361        PrimitiveKeyMaterialKind::NotKey => Ok(KeyMaterialKind::NotKey),
362        PrimitiveKeyMaterialKind::PublicKey => Ok(KeyMaterialKind::PublicKey),
363        PrimitiveKeyMaterialKind::PrivateKey => Ok(KeyMaterialKind::PrivateKey),
364        PrimitiveKeyMaterialKind::SymmetricKey => Ok(KeyMaterialKind::SymmetricKey),
365        _ => Err(MulticodecOperationError::RegistryInvariant),
366    }
367}
368
369#[cfg(test)]
370mod tests;