Skip to main content

reallyme_codec/
multicodec.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
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,
14    KeyMaterialKind as PrimitiveKeyMaterialKind, MULTICODEC_TABLE, VARIABLE_KEY_LENGTH,
15};
16use std::sync::OnceLock;
17
18const MAX_U64_VARINT_BYTES: usize = 10;
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 registry uses zero for both variable-length key material and entries
76/// where a key length does not apply. Keeping those meanings distinct here
77/// prevents boundary adapters from treating an algorithm identifier as a
78/// variable-length key codec.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[non_exhaustive]
81pub enum MulticodecLength {
82    /// Values have the exact byte length carried by the variant.
83    Fixed(usize),
84    /// Valid value lengths vary within limits defined by the owning codec.
85    Variable,
86    /// A value length is not meaningful for this registry entry.
87    NotApplicable,
88}
89
90/// Borrowed semantic metadata for one supported multicodec entry.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct MulticodecSpec<'a> {
93    name: &'a str,
94    tag: CodecTag,
95    key_material: KeyMaterialKind,
96    algorithm_name: &'a str,
97    code: &'a [u8],
98    prefix: &'a [u8],
99    length: MulticodecLength,
100}
101
102/// Semantic result for prefix lookup by payload bytes.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct MulticodecLookup<'a> {
105    name: &'a str,
106    prefix_length: usize,
107    metadata: MulticodecSpec<'a>,
108}
109
110/// Semantic result for the supported multicodec table.
111///
112/// This owner deliberately does not implement `Clone`: duplicating its vector
113/// would allocate outside the fallible, typed construction path.
114#[derive(Debug, PartialEq, Eq)]
115pub struct MulticodecTable<'a> {
116    entries: Vec<MulticodecSpec<'a>>,
117}
118
119impl<'a> MulticodecSpec<'a> {
120    /// Return the canonical registry name.
121    pub const fn name(&self) -> &'a str {
122        self.name
123    }
124
125    /// Return the semantic registry class.
126    pub const fn tag(&self) -> CodecTag {
127        self.tag
128    }
129
130    /// Return the key-material classification.
131    pub const fn key_material(&self) -> KeyMaterialKind {
132        self.key_material
133    }
134
135    /// Return the public algorithm name.
136    pub const fn algorithm_name(&self) -> &'a str {
137        self.algorithm_name
138    }
139
140    /// Return the encoded multicodec varint bytes.
141    pub const fn code(&self) -> &'a [u8] {
142        self.code
143    }
144
145    /// Return the prefix bytes matched or prepended by codec operations.
146    pub const fn prefix(&self) -> &'a [u8] {
147        self.prefix
148    }
149
150    /// Return the semantic length rule for values described by this entry.
151    pub const fn length(&self) -> MulticodecLength {
152        self.length
153    }
154}
155
156impl<'a> MulticodecLookup<'a> {
157    /// Return the canonical name of the matched entry.
158    pub const fn name(&self) -> &'a str {
159        self.name
160    }
161
162    /// Return the number of matched prefix bytes.
163    pub const fn prefix_length(&self) -> usize {
164        self.prefix_length
165    }
166
167    /// Return the complete metadata for the matched entry.
168    pub const fn metadata(&self) -> &MulticodecSpec<'a> {
169        &self.metadata
170    }
171}
172
173impl<'a> MulticodecTable<'a> {
174    /// Return the supported entries in stable registry order.
175    pub fn entries(&self) -> &[MulticodecSpec<'a>] {
176        self.entries.as_slice()
177    }
178}
179
180/// Resolve a canonical multicodec name to semantic registry metadata.
181pub fn prefix_for_name(name: &str) -> Result<MulticodecSpec<'static>, MulticodecOperationError> {
182    validate_registry()?;
183    let Some((canonical_name, spec)) = find_codec_spec(name) else {
184        return Err(MulticodecOperationError::UnknownName);
185    };
186    multicodec_spec(
187        canonical_name,
188        spec.tag,
189        spec.key_material,
190        spec.alg,
191        spec.codec,
192        spec.key_length,
193    )
194}
195
196/// Resolve the known multicodec prefix at the start of `value`.
197pub fn lookup_prefix(value: &[u8]) -> Result<MulticodecLookup<'static>, MulticodecOperationError> {
198    validate_registry()?;
199    let Some(found) = lookup_codec_prefix(value) else {
200        return Err(MulticodecOperationError::InvalidPrefix);
201    };
202    Ok(MulticodecLookup {
203        name: found.name,
204        prefix_length: found.codec.len(),
205        metadata: multicodec_spec(
206            found.name,
207            found.tag,
208            found.key_material,
209            found.alg,
210            found.codec,
211            found.key_length,
212        )?,
213    })
214}
215
216/// Strip a known multicodec prefix, preserving `value` when no prefix matches.
217pub fn strip_prefix(value: &[u8]) -> Result<&[u8], MulticodecOperationError> {
218    match lookup_prefix(value) {
219        Ok(found) => value
220            .get(found.prefix_length()..)
221            .ok_or(MulticodecOperationError::RegistryInvariant),
222        Err(MulticodecOperationError::InvalidPrefix) => Ok(value),
223        Err(error) => Err(error),
224    }
225}
226
227/// Return all supported multicodec entries in stable registry order.
228pub fn supported_table() -> Result<MulticodecTable<'static>, MulticodecOperationError> {
229    validate_registry()?;
230    let mut entries = table_entries_with_capacity(MULTICODEC_TABLE.len())?;
231    for (name, spec) in MULTICODEC_TABLE {
232        entries.push(multicodec_spec(
233            name,
234            spec.tag,
235            spec.key_material,
236            spec.alg,
237            spec.codec,
238            spec.key_length,
239        )?);
240    }
241    Ok(MulticodecTable { entries })
242}
243
244fn table_entries_with_capacity(
245    capacity: usize,
246) -> Result<Vec<MulticodecSpec<'static>>, MulticodecOperationError> {
247    let mut entries = Vec::new();
248    entries
249        .try_reserve(capacity)
250        .map_err(|_| MulticodecOperationError::AllocationFailure)?;
251    Ok(entries)
252}
253
254fn find_codec_spec(codec_name: &str) -> Option<(&'static str, &'static CodecSpec)> {
255    MULTICODEC_TABLE
256        .iter()
257        .find(|(name, _)| *name == codec_name)
258        .map(|(name, spec)| (*name, spec))
259}
260
261fn validate_registry() -> Result<(), MulticodecOperationError> {
262    *REGISTRY_VALIDITY.get_or_init(validate_registry_uncached)
263}
264
265fn validate_registry_uncached() -> Result<(), MulticodecOperationError> {
266    validate_registry_entries(MULTICODEC_TABLE)
267}
268
269fn validate_registry_entries(
270    entries: &[(&str, CodecSpec)],
271) -> Result<(), MulticodecOperationError> {
272    if entries.is_empty() {
273        return Err(MulticodecOperationError::RegistryInvariant);
274    }
275    for (index, (name, spec)) in entries.iter().enumerate() {
276        if name.is_empty() || spec.alg.is_empty() || !is_canonical_u64_varint(spec.codec) {
277            return Err(MulticodecOperationError::RegistryInvariant);
278        }
279
280        let tag = semantic_codec_tag(spec.tag)?;
281        let key_material = semantic_key_material_kind(spec.key_material)?;
282        let is_key_tag = tag == CodecTag::Key;
283        let carries_key_material = key_material != KeyMaterialKind::NotKey;
284        if is_key_tag != carries_key_material {
285            return Err(MulticodecOperationError::RegistryInvariant);
286        }
287
288        let next_index = index
289            .checked_add(1)
290            .ok_or(MulticodecOperationError::RegistryInvariant)?;
291        let remaining = entries
292            .get(next_index..)
293            .ok_or(MulticodecOperationError::RegistryInvariant)?;
294        for (other_name, other_spec) in remaining {
295            let ambiguous_prefix = spec.codec.starts_with(other_spec.codec)
296                || other_spec.codec.starts_with(spec.codec);
297            if name == other_name || ambiguous_prefix {
298                return Err(MulticodecOperationError::RegistryInvariant);
299            }
300        }
301    }
302    Ok(())
303}
304
305fn is_canonical_u64_varint(value: &[u8]) -> bool {
306    if value.len() > MAX_U64_VARINT_BYTES {
307        return false;
308    }
309    let Some((&last, leading)) = value.split_last() else {
310        return false;
311    };
312    if last & 0x80 != 0 || leading.iter().any(|byte| byte & 0x80 == 0) {
313        return false;
314    }
315    if !leading.is_empty() && last & 0x7f == 0 {
316        return false;
317    }
318    value.len() < MAX_U64_VARINT_BYTES || last <= 1
319}
320
321fn multicodec_spec(
322    name: &'static str,
323    tag: PrimitiveCodecTag,
324    key_material: PrimitiveKeyMaterialKind,
325    algorithm_name: &'static str,
326    prefix: &'static [u8],
327    key_length: usize,
328) -> Result<MulticodecSpec<'static>, MulticodecOperationError> {
329    let tag = semantic_codec_tag(tag)?;
330    let key_material = semantic_key_material_kind(key_material)?;
331    let length = if key_length != VARIABLE_KEY_LENGTH {
332        MulticodecLength::Fixed(key_length)
333    } else if key_material == KeyMaterialKind::NotKey {
334        MulticodecLength::NotApplicable
335    } else {
336        MulticodecLength::Variable
337    };
338    Ok(MulticodecSpec {
339        name,
340        tag,
341        key_material,
342        algorithm_name,
343        code: prefix,
344        prefix,
345        length,
346    })
347}
348
349fn semantic_codec_tag(tag: PrimitiveCodecTag) -> Result<CodecTag, MulticodecOperationError> {
350    match tag {
351        PrimitiveCodecTag::Encryption => Ok(CodecTag::Encryption),
352        PrimitiveCodecTag::Hash => Ok(CodecTag::Hash),
353        PrimitiveCodecTag::Key => Ok(CodecTag::Key),
354        PrimitiveCodecTag::Multihash => Ok(CodecTag::Multihash),
355        PrimitiveCodecTag::Multikey => Ok(CodecTag::Multikey),
356        _ => Err(MulticodecOperationError::RegistryInvariant),
357    }
358}
359
360fn semantic_key_material_kind(
361    kind: PrimitiveKeyMaterialKind,
362) -> Result<KeyMaterialKind, MulticodecOperationError> {
363    match kind {
364        PrimitiveKeyMaterialKind::NotKey => Ok(KeyMaterialKind::NotKey),
365        PrimitiveKeyMaterialKind::PublicKey => Ok(KeyMaterialKind::PublicKey),
366        PrimitiveKeyMaterialKind::PrivateKey => Ok(KeyMaterialKind::PrivateKey),
367        PrimitiveKeyMaterialKind::SymmetricKey => Ok(KeyMaterialKind::SymmetricKey),
368        _ => Err(MulticodecOperationError::RegistryInvariant),
369    }
370}
371
372#[cfg(test)]
373mod tests;