Skip to main content

uor_addr/
label.rs

1//! `AddressLabel*` — UOR-ADDR's **common output shape** family
2//! (ARCHITECTURE.md "Common output shape"), one specialization per
3//! admissible σ-axis ([`crate::hash`]).
4//!
5//! The κ-label's wire-format byte layout follows the architecture
6//! document's structural formula
7//!
8//! ```text
9//! SITE_COUNT = H::LABEL_PREFIX.len() + 1 + 2 × H::OUTPUT_BYTES
10//! ```
11//!
12//! parameterized on the realization's selected hash axis `H: AddrHash`.
13//! The output space is **π_0-only** by structural property of the
14//! σ-projection + hex-serialization composition; `χ(N(C)) = SITE_COUNT`;
15//! `β_0 = SITE_COUNT`; `β_k = 0` for `k ≥ 1`.
16//!
17//! Each admissible axis has its own [`output_shape!`] specialization with
18//! its own IRI suffix and `SITE_COUNT` (sha256 / blake3 = 71, sha3-256 =
19//! 73, keccak256 = 74). The IRI specializes per axis so that two
20//! realizations binding different `H` selections produce distinct typed
21//! reference vocabularies at the IRI level (the framework's typed-iso
22//! commitment per ADR-001 + ADR-017).
23//!
24//! The runtime κ-label carrier [`KappaLabel`] is generic over the label
25//! byte width `N`, so a single value type carries every axis's label; the
26//! width lives in the type (`KappaLabel<71>` for sha256, `KappaLabel<74>`
27//! for keccak256, …).
28
29use prism::pipeline::{output_shape, ConstraintRef};
30
31use crate::hash::{label_bytes, AddrHash};
32use prism::crypto::Sha256Hasher;
33
34/// **The wire-format address byte width** under the default σ-axis
35/// `H = Sha256Hasher`: `len("sha256") + 1 + 2 × 32 = 71`.
36pub const ADDRESS_LABEL_BYTES: usize = label_bytes(Sha256Hasher::LABEL_PREFIX, 32);
37
38/// Build the `[Site{0}, Site{1}, …, Site{N-1}]` constraint array — one
39/// disjoint `ConstraintRef::Site` per wire-format κ-label byte position.
40#[must_use]
41pub const fn site_constraints<const N: usize>() -> [ConstraintRef; N] {
42    let mut sites = [ConstraintRef::Site { position: 0 }; N];
43    let mut i = 0;
44    while i < N {
45        sites[i] = ConstraintRef::Site { position: i as u32 };
46        i += 1;
47    }
48    sites
49}
50
51// ── Per-axis output shapes. Each declares `SITE_COUNT` disjoint `Site`
52//    constraints — one per wire-format byte position — and a per-axis IRI
53//    suffix carrying the algorithm token. ──
54
55static SHA256_SITES: [ConstraintRef; 71] = site_constraints::<71>();
56output_shape! {
57    pub struct AddressLabelSha256;
58    impl ConstrainedTypeShape for AddressLabelSha256 {
59        const IRI: &'static str = "https://uor.foundation/addr/AddressLabel/sha256";
60        const SITE_COUNT: usize = 71;
61        const CONSTRAINTS: &'static [ConstraintRef] = &SHA256_SITES;
62    }
63}
64
65static BLAKE3_SITES: [ConstraintRef; 71] = site_constraints::<71>();
66output_shape! {
67    pub struct AddressLabelBlake3;
68    impl ConstrainedTypeShape for AddressLabelBlake3 {
69        const IRI: &'static str = "https://uor.foundation/addr/AddressLabel/blake3";
70        const SITE_COUNT: usize = 71;
71        const CONSTRAINTS: &'static [ConstraintRef] = &BLAKE3_SITES;
72    }
73}
74
75static SHA3_256_SITES: [ConstraintRef; 73] = site_constraints::<73>();
76output_shape! {
77    pub struct AddressLabelSha3_256;
78    impl ConstrainedTypeShape for AddressLabelSha3_256 {
79        const IRI: &'static str = "https://uor.foundation/addr/AddressLabel/sha3-256";
80        const SITE_COUNT: usize = 73;
81        const CONSTRAINTS: &'static [ConstraintRef] = &SHA3_256_SITES;
82    }
83}
84
85static KECCAK256_SITES: [ConstraintRef; 74] = site_constraints::<74>();
86output_shape! {
87    pub struct AddressLabelKeccak256;
88    impl ConstrainedTypeShape for AddressLabelKeccak256 {
89        const IRI: &'static str = "https://uor.foundation/addr/AddressLabel/keccak256";
90        const SITE_COUNT: usize = 74;
91        const CONSTRAINTS: &'static [ConstraintRef] = &KECCAK256_SITES;
92    }
93}
94
95static SHA512_SITES: [ConstraintRef; 135] = site_constraints::<135>();
96output_shape! {
97    pub struct AddressLabelSha512;
98    impl ConstrainedTypeShape for AddressLabelSha512 {
99        const IRI: &'static str = "https://uor.foundation/addr/AddressLabel/sha512";
100        const SITE_COUNT: usize = 135;
101        const CONSTRAINTS: &'static [ConstraintRef] = &SHA512_SITES;
102    }
103}
104
105/// The default-axis output shape (`H = Sha256Hasher`). Realizations'
106/// `address()` entry point binds this; `address_blake3` / `address_sha3_256`
107/// / `address_keccak256` bind the corresponding per-axis shape.
108pub type AddressLabel = AddressLabelSha256;
109
110// ── Composition output shapes (ADR-061 §(2)): one per categorical
111//    operation × σ-axis. The composed κ-label is a standard axis-width
112//    label distinguished from its operands by the per-op realization IRI. ──
113
114output_shape! {
115    pub struct CompositionLabelG2Sha256;
116    impl ConstrainedTypeShape for CompositionLabelG2Sha256 {
117        const IRI: &'static str = "https://uor.foundation/addr/composition/g2-product/sha256";
118        const SITE_COUNT: usize = 71;
119        const CONSTRAINTS: &'static [ConstraintRef] = &SHA256_SITES;
120    }
121}
122
123output_shape! {
124    pub struct CompositionLabelG2Blake3;
125    impl ConstrainedTypeShape for CompositionLabelG2Blake3 {
126        const IRI: &'static str = "https://uor.foundation/addr/composition/g2-product/blake3";
127        const SITE_COUNT: usize = 71;
128        const CONSTRAINTS: &'static [ConstraintRef] = &BLAKE3_SITES;
129    }
130}
131
132output_shape! {
133    pub struct CompositionLabelG2Sha3_256;
134    impl ConstrainedTypeShape for CompositionLabelG2Sha3_256 {
135        const IRI: &'static str = "https://uor.foundation/addr/composition/g2-product/sha3-256";
136        const SITE_COUNT: usize = 73;
137        const CONSTRAINTS: &'static [ConstraintRef] = &SHA3_256_SITES;
138    }
139}
140
141output_shape! {
142    pub struct CompositionLabelG2Keccak256;
143    impl ConstrainedTypeShape for CompositionLabelG2Keccak256 {
144        const IRI: &'static str = "https://uor.foundation/addr/composition/g2-product/keccak256";
145        const SITE_COUNT: usize = 74;
146        const CONSTRAINTS: &'static [ConstraintRef] = &KECCAK256_SITES;
147    }
148}
149
150output_shape! {
151    pub struct CompositionLabelG2Sha512;
152    impl ConstrainedTypeShape for CompositionLabelG2Sha512 {
153        const IRI: &'static str = "https://uor.foundation/addr/composition/g2-product/sha512";
154        const SITE_COUNT: usize = 135;
155        const CONSTRAINTS: &'static [ConstraintRef] = &SHA512_SITES;
156    }
157}
158
159output_shape! {
160    pub struct CompositionLabelF4Sha256;
161    impl ConstrainedTypeShape for CompositionLabelF4Sha256 {
162        const IRI: &'static str = "https://uor.foundation/addr/composition/f4-quotient/sha256";
163        const SITE_COUNT: usize = 71;
164        const CONSTRAINTS: &'static [ConstraintRef] = &SHA256_SITES;
165    }
166}
167
168output_shape! {
169    pub struct CompositionLabelF4Blake3;
170    impl ConstrainedTypeShape for CompositionLabelF4Blake3 {
171        const IRI: &'static str = "https://uor.foundation/addr/composition/f4-quotient/blake3";
172        const SITE_COUNT: usize = 71;
173        const CONSTRAINTS: &'static [ConstraintRef] = &BLAKE3_SITES;
174    }
175}
176
177output_shape! {
178    pub struct CompositionLabelF4Sha3_256;
179    impl ConstrainedTypeShape for CompositionLabelF4Sha3_256 {
180        const IRI: &'static str = "https://uor.foundation/addr/composition/f4-quotient/sha3-256";
181        const SITE_COUNT: usize = 73;
182        const CONSTRAINTS: &'static [ConstraintRef] = &SHA3_256_SITES;
183    }
184}
185
186output_shape! {
187    pub struct CompositionLabelF4Keccak256;
188    impl ConstrainedTypeShape for CompositionLabelF4Keccak256 {
189        const IRI: &'static str = "https://uor.foundation/addr/composition/f4-quotient/keccak256";
190        const SITE_COUNT: usize = 74;
191        const CONSTRAINTS: &'static [ConstraintRef] = &KECCAK256_SITES;
192    }
193}
194
195output_shape! {
196    pub struct CompositionLabelF4Sha512;
197    impl ConstrainedTypeShape for CompositionLabelF4Sha512 {
198        const IRI: &'static str = "https://uor.foundation/addr/composition/f4-quotient/sha512";
199        const SITE_COUNT: usize = 135;
200        const CONSTRAINTS: &'static [ConstraintRef] = &SHA512_SITES;
201    }
202}
203
204output_shape! {
205    pub struct CompositionLabelE6Sha256;
206    impl ConstrainedTypeShape for CompositionLabelE6Sha256 {
207        const IRI: &'static str = "https://uor.foundation/addr/composition/e6-filtration/sha256";
208        const SITE_COUNT: usize = 71;
209        const CONSTRAINTS: &'static [ConstraintRef] = &SHA256_SITES;
210    }
211}
212
213output_shape! {
214    pub struct CompositionLabelE6Blake3;
215    impl ConstrainedTypeShape for CompositionLabelE6Blake3 {
216        const IRI: &'static str = "https://uor.foundation/addr/composition/e6-filtration/blake3";
217        const SITE_COUNT: usize = 71;
218        const CONSTRAINTS: &'static [ConstraintRef] = &BLAKE3_SITES;
219    }
220}
221
222output_shape! {
223    pub struct CompositionLabelE6Sha3_256;
224    impl ConstrainedTypeShape for CompositionLabelE6Sha3_256 {
225        const IRI: &'static str = "https://uor.foundation/addr/composition/e6-filtration/sha3-256";
226        const SITE_COUNT: usize = 73;
227        const CONSTRAINTS: &'static [ConstraintRef] = &SHA3_256_SITES;
228    }
229}
230
231output_shape! {
232    pub struct CompositionLabelE6Keccak256;
233    impl ConstrainedTypeShape for CompositionLabelE6Keccak256 {
234        const IRI: &'static str = "https://uor.foundation/addr/composition/e6-filtration/keccak256";
235        const SITE_COUNT: usize = 74;
236        const CONSTRAINTS: &'static [ConstraintRef] = &KECCAK256_SITES;
237    }
238}
239
240output_shape! {
241    pub struct CompositionLabelE6Sha512;
242    impl ConstrainedTypeShape for CompositionLabelE6Sha512 {
243        const IRI: &'static str = "https://uor.foundation/addr/composition/e6-filtration/sha512";
244        const SITE_COUNT: usize = 135;
245        const CONSTRAINTS: &'static [ConstraintRef] = &SHA512_SITES;
246    }
247}
248
249output_shape! {
250    pub struct CompositionLabelE7Sha256;
251    impl ConstrainedTypeShape for CompositionLabelE7Sha256 {
252        const IRI: &'static str = "https://uor.foundation/addr/composition/e7-augmentation/sha256";
253        const SITE_COUNT: usize = 71;
254        const CONSTRAINTS: &'static [ConstraintRef] = &SHA256_SITES;
255    }
256}
257
258output_shape! {
259    pub struct CompositionLabelE7Blake3;
260    impl ConstrainedTypeShape for CompositionLabelE7Blake3 {
261        const IRI: &'static str = "https://uor.foundation/addr/composition/e7-augmentation/blake3";
262        const SITE_COUNT: usize = 71;
263        const CONSTRAINTS: &'static [ConstraintRef] = &BLAKE3_SITES;
264    }
265}
266
267output_shape! {
268    pub struct CompositionLabelE7Sha3_256;
269    impl ConstrainedTypeShape for CompositionLabelE7Sha3_256 {
270        const IRI: &'static str = "https://uor.foundation/addr/composition/e7-augmentation/sha3-256";
271        const SITE_COUNT: usize = 73;
272        const CONSTRAINTS: &'static [ConstraintRef] = &SHA3_256_SITES;
273    }
274}
275
276output_shape! {
277    pub struct CompositionLabelE7Keccak256;
278    impl ConstrainedTypeShape for CompositionLabelE7Keccak256 {
279        const IRI: &'static str = "https://uor.foundation/addr/composition/e7-augmentation/keccak256";
280        const SITE_COUNT: usize = 74;
281        const CONSTRAINTS: &'static [ConstraintRef] = &KECCAK256_SITES;
282    }
283}
284
285output_shape! {
286    pub struct CompositionLabelE7Sha512;
287    impl ConstrainedTypeShape for CompositionLabelE7Sha512 {
288        const IRI: &'static str = "https://uor.foundation/addr/composition/e7-augmentation/sha512";
289        const SITE_COUNT: usize = 135;
290        const CONSTRAINTS: &'static [ConstraintRef] = &SHA512_SITES;
291    }
292}
293
294output_shape! {
295    pub struct CompositionLabelE8Sha256;
296    impl ConstrainedTypeShape for CompositionLabelE8Sha256 {
297        const IRI: &'static str = "https://uor.foundation/addr/composition/e8-embedding/sha256";
298        const SITE_COUNT: usize = 71;
299        const CONSTRAINTS: &'static [ConstraintRef] = &SHA256_SITES;
300    }
301}
302
303output_shape! {
304    pub struct CompositionLabelE8Blake3;
305    impl ConstrainedTypeShape for CompositionLabelE8Blake3 {
306        const IRI: &'static str = "https://uor.foundation/addr/composition/e8-embedding/blake3";
307        const SITE_COUNT: usize = 71;
308        const CONSTRAINTS: &'static [ConstraintRef] = &BLAKE3_SITES;
309    }
310}
311
312output_shape! {
313    pub struct CompositionLabelE8Sha3_256;
314    impl ConstrainedTypeShape for CompositionLabelE8Sha3_256 {
315        const IRI: &'static str = "https://uor.foundation/addr/composition/e8-embedding/sha3-256";
316        const SITE_COUNT: usize = 73;
317        const CONSTRAINTS: &'static [ConstraintRef] = &SHA3_256_SITES;
318    }
319}
320
321output_shape! {
322    pub struct CompositionLabelE8Keccak256;
323    impl ConstrainedTypeShape for CompositionLabelE8Keccak256 {
324        const IRI: &'static str = "https://uor.foundation/addr/composition/e8-embedding/keccak256";
325        const SITE_COUNT: usize = 74;
326        const CONSTRAINTS: &'static [ConstraintRef] = &KECCAK256_SITES;
327    }
328}
329
330output_shape! {
331    pub struct CompositionLabelE8Sha512;
332    impl ConstrainedTypeShape for CompositionLabelE8Sha512 {
333        const IRI: &'static str = "https://uor.foundation/addr/composition/e8-embedding/sha512";
334        const SITE_COUNT: usize = 135;
335        const CONSTRAINTS: &'static [ConstraintRef] = &SHA512_SITES;
336    }
337}
338
339/// **The runtime κ-label carrier** — the `N`-byte ASCII
340/// `<algorithm>:<lowercase-hex>` wire-format byte sequence.
341///
342/// `KappaLabel` carries the κ-derivation output of the ψ-pipeline: the
343/// algorithm prefix, a `:` separator, and the lowercase-hex serialization
344/// of the σ-projection digest. The width `N` is the axis's
345/// [`AddrHash::LABEL_BYTES`] (71 for sha256 / blake3, 73 for sha3-256, 74
346/// for keccak256). The constructor [`KappaLabel::from_bytes`] validates
347/// length (= `N`) and ASCII purity; downstream methods can therefore
348/// project to `&str` infallibly.
349///
350/// `Copy + Eq + Hash` — callers may freely thread the κ-label through hash
351/// maps, identity comparisons, and pass-by-value contexts without any
352/// allocator. `Deref<Target = str>` provides the usual `&str` methods.
353#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
354pub struct KappaLabel<const N: usize>([u8; N]);
355
356impl<const N: usize> KappaLabel<N> {
357    /// Build a `KappaLabel<N>` from an `N`-byte ASCII slice. Returns
358    /// `LabelDecodeError` if `bytes.len() != N` or `bytes` contains a
359    /// non-ASCII byte.
360    ///
361    /// The ψ-pipeline emits ASCII-only bytes by construction (the
362    /// algorithm prefix plus lowercase-hex). Defense-in-depth: the
363    /// constructor still validates so a substrate-corrupted byte sequence
364    /// cannot smuggle a non-ASCII `KappaLabel` into the typed surface.
365    ///
366    /// # Errors
367    ///
368    /// - [`LabelDecodeError::WrongLength`] — `bytes.len() != N`.
369    /// - [`LabelDecodeError::NonAscii`] — `bytes` contains a byte ≥ 0x80.
370    pub fn from_bytes(bytes: &[u8]) -> Result<Self, LabelDecodeError> {
371        if bytes.len() != N {
372            return Err(LabelDecodeError::WrongLength);
373        }
374        let mut buf = [0u8; N];
375        for (dst, &src) in buf.iter_mut().zip(bytes.iter()) {
376            if !src.is_ascii() {
377                return Err(LabelDecodeError::NonAscii);
378            }
379            *dst = src;
380        }
381        Ok(Self(buf))
382    }
383
384    /// Borrow the κ-label as an `N`-byte array.
385    #[must_use]
386    pub fn as_array(&self) -> &[u8; N] {
387        &self.0
388    }
389
390    /// Borrow the κ-label as a `&str`. Infallible — the carrier is ASCII
391    /// by construction (validated at [`KappaLabel::from_bytes`]).
392    #[must_use]
393    pub fn as_str(&self) -> &str {
394        core::str::from_utf8(&self.0).expect("KappaLabel is ASCII by construction")
395    }
396
397    /// Borrow the κ-label as a byte slice.
398    #[must_use]
399    pub fn as_bytes(&self) -> &[u8] {
400        &self.0
401    }
402
403    /// The σ-axis token at the head of the κ-label — the substring before
404    /// the `:` separator (`"sha256"`, `"blake3"`, `"sha3-256"`,
405    /// `"keccak256"`, `"sha512"`). Returns `None` if the label carries no
406    /// `:` (unreachable for a pipeline-emitted κ-label).
407    #[must_use]
408    pub fn sigma_axis(&self) -> Option<&str> {
409        self.as_str().split_once(':').map(|(axis, _)| axis)
410    }
411
412    /// The lowercase-hex digest body — the substring after the `:`
413    /// separator. Returns `None` if the label carries no `:`.
414    #[must_use]
415    pub fn sigma_axis_digest_hex(&self) -> Option<&str> {
416        self.as_str().split_once(':').map(|(_, hex)| hex)
417    }
418}
419
420impl<const N: usize> core::ops::Deref for KappaLabel<N> {
421    type Target = str;
422    fn deref(&self) -> &str {
423        self.as_str()
424    }
425}
426
427impl<const N: usize> AsRef<str> for KappaLabel<N> {
428    fn as_ref(&self) -> &str {
429        self.as_str()
430    }
431}
432
433impl<const N: usize> AsRef<[u8]> for KappaLabel<N> {
434    fn as_ref(&self) -> &[u8] {
435        &self.0
436    }
437}
438
439impl<const N: usize> core::fmt::Display for KappaLabel<N> {
440    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
441        f.write_str(self.as_str())
442    }
443}
444
445impl<const N: usize> core::fmt::Debug for KappaLabel<N> {
446    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
447        f.debug_tuple("KappaLabel").field(&self.as_str()).finish()
448    }
449}
450
451impl<const N: usize> PartialEq<str> for KappaLabel<N> {
452    fn eq(&self, other: &str) -> bool {
453        self.as_str() == other
454    }
455}
456
457impl<const N: usize> PartialEq<&str> for KappaLabel<N> {
458    fn eq(&self, other: &&str) -> bool {
459        self.as_str() == *other
460    }
461}
462
463impl<const N: usize> PartialEq<KappaLabel<N>> for str {
464    fn eq(&self, other: &KappaLabel<N>) -> bool {
465        self == other.as_str()
466    }
467}
468
469impl<const N: usize> PartialEq<KappaLabel<N>> for &str {
470    fn eq(&self, other: &KappaLabel<N>) -> bool {
471        *self == other.as_str()
472    }
473}
474
475/// Decoding failures from [`KappaLabel::from_bytes`].
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477pub enum LabelDecodeError {
478    /// `bytes.len() != N`.
479    WrongLength,
480    /// `bytes` contains a non-ASCII byte (≥ 0x80).
481    NonAscii,
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use prism::crypto::{Blake3Hasher, Keccak256Hasher, Sha3_256Hasher};
488    use prism::pipeline::ConstrainedTypeShape;
489
490    #[test]
491    fn site_count_matches_wire_format_byte_width_per_axis() {
492        assert_eq!(
493            <AddressLabelSha256 as ConstrainedTypeShape>::SITE_COUNT,
494            Sha256Hasher::LABEL_BYTES
495        );
496        assert_eq!(
497            <AddressLabelBlake3 as ConstrainedTypeShape>::SITE_COUNT,
498            Blake3Hasher::LABEL_BYTES
499        );
500        assert_eq!(
501            <AddressLabelSha3_256 as ConstrainedTypeShape>::SITE_COUNT,
502            Sha3_256Hasher::LABEL_BYTES
503        );
504        assert_eq!(
505            <AddressLabelKeccak256 as ConstrainedTypeShape>::SITE_COUNT,
506            Keccak256Hasher::LABEL_BYTES
507        );
508    }
509
510    #[test]
511    fn iri_carries_axis_suffix_per_architecture() {
512        assert_eq!(
513            <AddressLabelSha256 as ConstrainedTypeShape>::IRI,
514            "https://uor.foundation/addr/AddressLabel/sha256"
515        );
516        assert_eq!(
517            <AddressLabelKeccak256 as ConstrainedTypeShape>::IRI,
518            "https://uor.foundation/addr/AddressLabel/keccak256"
519        );
520    }
521
522    #[test]
523    fn each_shape_carries_disjoint_site_constraints() {
524        let cs = <AddressLabelSha3_256 as ConstrainedTypeShape>::CONSTRAINTS;
525        assert_eq!(cs.len(), 73);
526        for (i, c) in cs.iter().enumerate() {
527            match c {
528                ConstraintRef::Site { position } => assert_eq!(*position, i as u32),
529                _ => panic!("expected Site constraint at index {i}"),
530            }
531        }
532    }
533
534    #[test]
535    fn kappa_label_from_bytes_round_trips_valid_input() {
536        let bytes = b"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
537        let label = KappaLabel::<71>::from_bytes(bytes).expect("valid");
538        assert_eq!(label.as_str(), core::str::from_utf8(bytes).unwrap());
539        assert_eq!(label.as_bytes(), bytes);
540        assert_eq!(label.as_array(), bytes);
541        assert!(label.starts_with("sha256:"));
542        assert_eq!(label.len(), ADDRESS_LABEL_BYTES);
543    }
544
545    #[test]
546    fn kappa_label_rejects_wrong_length() {
547        let err = KappaLabel::<71>::from_bytes(b"too short").expect_err("rejects");
548        assert_eq!(err, LabelDecodeError::WrongLength);
549    }
550
551    #[test]
552    fn kappa_label_rejects_non_ascii_byte() {
553        let mut bytes = [b'a'; 71];
554        bytes[3] = 0x80;
555        let err = KappaLabel::<71>::from_bytes(&bytes).expect_err("rejects");
556        assert_eq!(err, LabelDecodeError::NonAscii);
557    }
558
559    #[test]
560    fn kappa_label_equality_against_str() {
561        let bytes = b"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
562        let label = KappaLabel::<71>::from_bytes(bytes).expect("valid");
563        let s: &str = core::str::from_utf8(bytes).unwrap();
564        assert_eq!(label, *s);
565        assert_eq!(label, s);
566        assert_eq!(s, label);
567    }
568}