Skip to main content

icydb_core/value/
storage_key.rs

1//! Module: value::storage_key
2//! Responsibility: fixed-width scalar key encoding for persistent ordering.
3//! Does not own: typed primary-key semantics (`Id<E>`) or query coercion rules.
4//! Boundary: shared by value normalization and data/index persistence layers.
5//!
6//! `StorageKey` is a storage-normalized scalar and MUST NOT be used as a
7//! public primary-key abstraction.
8
9#![expect(clippy::cast_possible_truncation)]
10
11use super::Value;
12use crate::{
13    error::InternalError,
14    traits::Repr,
15    types::{Account, Principal, Subaccount, Timestamp, Ulid},
16};
17use candid::CandidType;
18use serde::{Deserialize, Serialize};
19use std::cmp::Ordering;
20use thiserror::Error as ThisError;
21
22//
23// StorageKeyEncodeError
24// Errors returned when encoding a storage key for persistence.
25//
26
27#[derive(Debug, ThisError)]
28pub enum StorageKeyEncodeError {
29    #[error("account owner principal exceeds max length: {len} bytes (limit {max})")]
30    AccountOwnerTooLarge { len: usize, max: usize },
31
32    #[error("account payload length mismatch: {len} bytes (expected {expected})")]
33    AccountLengthMismatch { len: usize, expected: usize },
34
35    #[error("value kind '{kind}' is not storage-key encodable")]
36    UnsupportedValueKind { kind: &'static str },
37
38    #[error("principal exceeds max length: {len} bytes (limit {max})")]
39    PrincipalTooLarge { len: usize, max: usize },
40}
41
42impl From<StorageKeyEncodeError> for InternalError {
43    fn from(err: StorageKeyEncodeError) -> Self {
44        Self::serialize_unsupported(err.to_string())
45    }
46}
47
48//
49// StorageKeyDecodeError
50// Errors returned when decoding a persisted storage key payload.
51//
52
53#[derive(Debug, ThisError)]
54pub enum StorageKeyDecodeError {
55    #[error("corrupted StorageKey: invalid size")]
56    InvalidSize,
57
58    #[error("corrupted StorageKey: invalid tag")]
59    InvalidTag,
60
61    #[error("corrupted StorageKey: invalid principal length")]
62    InvalidPrincipalLength,
63
64    #[error("corrupted StorageKey: non-zero {field} padding")]
65    NonZeroPadding { field: &'static str },
66
67    #[error("corrupted StorageKey: invalid account payload ({reason})")]
68    InvalidAccountPayload { reason: &'static str },
69}
70
71//
72// StorageKey
73//
74// Storage-normalized scalar key used by persistence and indexing.
75//
76// This type defines the *only* on-disk representation for scalar keys.
77// It is deliberately separated from typed primary-key values (`Id<E>`).
78//
79
80#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
81pub enum StorageKey {
82    Account(Account),
83    Int(i64),
84    Principal(Principal),
85    Subaccount(Subaccount),
86    Timestamp(Timestamp),
87    Uint(u64),
88    Ulid(Ulid),
89    Unit,
90}
91
92// Local helper to evaluate storage-key encodability from the scalar registry.
93macro_rules! value_is_storage_key_encodable_from_registry {
94    ( @args $value:expr; @entries $( ($scalar:ident, $family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
95        match $value {
96            $( $value_pat => $is_storage_key_encodable, )*
97            _ => false,
98        }
99    };
100}
101
102impl StorageKey {
103    // ── Variant tags (DO NOT reorder) ────────────────────────────────
104    pub(crate) const TAG_ACCOUNT: u8 = 0;
105    pub(crate) const TAG_INT: u8 = 1;
106    pub(crate) const TAG_PRINCIPAL: u8 = 2;
107    pub(crate) const TAG_SUBACCOUNT: u8 = 3;
108    pub(crate) const TAG_TIMESTAMP: u8 = 4;
109    pub(crate) const TAG_UINT: u8 = 5;
110    pub(crate) const TAG_ULID: u8 = 6;
111    pub(crate) const TAG_UNIT: u8 = 7;
112
113    /// Fixed serialized size in bytes (protocol invariant).
114    /// DO NOT CHANGE without migration.
115    pub const STORED_SIZE_BYTES: u64 = 64;
116    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
117
118    const TAG_SIZE: usize = 1;
119    pub(crate) const TAG_OFFSET: usize = 0;
120
121    pub(crate) const PAYLOAD_OFFSET: usize = Self::TAG_SIZE;
122    const PAYLOAD_SIZE: usize = Self::STORED_SIZE_USIZE - Self::TAG_SIZE;
123
124    pub(crate) const INT_SIZE: usize = 8;
125    pub(crate) const UINT_SIZE: usize = 8;
126    pub(crate) const TIMESTAMP_SIZE: usize = 8;
127    pub(crate) const ULID_SIZE: usize = 16;
128    pub(crate) const SUBACCOUNT_SIZE: usize = 32;
129    const ACCOUNT_MAX_SIZE: usize = 62;
130
131    pub const fn try_from_value(value: &Value) -> Result<Self, StorageKeyEncodeError> {
132        // Storage encodability is a persistent compatibility contract.
133        // Changing admission is a breaking change and may require index migration.
134        // This is intentionally distinct from schema keyability.
135        let is_storage_key_encodable =
136            scalar_registry!(value_is_storage_key_encodable_from_registry, value);
137        if !is_storage_key_encodable {
138            return Err(StorageKeyEncodeError::UnsupportedValueKind {
139                kind: Self::value_kind_label(value),
140            });
141        }
142
143        match value {
144            Value::Account(v) => Ok(Self::Account(*v)),
145            Value::Int(v) => Ok(Self::Int(*v)),
146            Value::Principal(v) => Ok(Self::Principal(*v)),
147            Value::Subaccount(v) => Ok(Self::Subaccount(*v)),
148            Value::Timestamp(v) => Ok(Self::Timestamp(*v)),
149            Value::Uint(v) => Ok(Self::Uint(*v)),
150            Value::Ulid(v) => Ok(Self::Ulid(*v)),
151            Value::Unit => Ok(Self::Unit),
152
153            _ => Err(StorageKeyEncodeError::UnsupportedValueKind {
154                kind: Self::value_kind_label(value),
155            }),
156        }
157    }
158
159    const fn value_kind_label(value: &Value) -> &'static str {
160        match value {
161            Value::Account(_) => "Account",
162            Value::Blob(_) => "Blob",
163            Value::Bool(_) => "Bool",
164            Value::Date(_) => "Date",
165            Value::Decimal(_) => "Decimal",
166            Value::Duration(_) => "Duration",
167            Value::Enum(_) => "Enum",
168            Value::Float32(_) => "Float32",
169            Value::Float64(_) => "Float64",
170            Value::Int(_) => "Int",
171            Value::Int128(_) => "Int128",
172            Value::IntBig(_) => "IntBig",
173            Value::List(_) => "List",
174            Value::Map(_) => "Map",
175            Value::Null => "Null",
176            Value::Principal(_) => "Principal",
177            Value::Subaccount(_) => "Subaccount",
178            Value::Text(_) => "Text",
179            Value::Timestamp(_) => "Timestamp",
180            Value::Uint(_) => "Uint",
181            Value::Uint128(_) => "Uint128",
182            Value::UintBig(_) => "UintBig",
183            Value::Ulid(_) => "Ulid",
184            Value::Unit => "Unit",
185        }
186    }
187
188    const fn tag(&self) -> u8 {
189        match self {
190            Self::Account(_) => Self::TAG_ACCOUNT,
191            Self::Int(_) => Self::TAG_INT,
192            Self::Principal(_) => Self::TAG_PRINCIPAL,
193            Self::Subaccount(_) => Self::TAG_SUBACCOUNT,
194            Self::Timestamp(_) => Self::TAG_TIMESTAMP,
195            Self::Uint(_) => Self::TAG_UINT,
196            Self::Ulid(_) => Self::TAG_ULID,
197            Self::Unit => Self::TAG_UNIT,
198        }
199    }
200
201    /// Sentinel key representing the maximum storable value.
202    #[must_use]
203    pub fn max_storable() -> Self {
204        Self::Account(Account::max_storable())
205    }
206
207    /// Global minimum key for scan bounds.
208    pub const MIN: Self = Self::Account(Account::from_parts(Principal::from_slice(&[]), None));
209
210    #[must_use]
211    pub const fn lower_bound() -> Self {
212        Self::MIN
213    }
214
215    #[must_use]
216    pub const fn upper_bound() -> Self {
217        Self::Unit
218    }
219
220    const fn variant_rank(&self) -> u8 {
221        self.tag()
222    }
223
224    const fn from_account_encode_error(
225        err: crate::types::AccountEncodeError,
226    ) -> StorageKeyEncodeError {
227        match err {
228            crate::types::AccountEncodeError::OwnerEncode(inner) => {
229                Self::from_principal_encode_error(inner)
230            }
231            crate::types::AccountEncodeError::OwnerTooLarge { len, max } => {
232                StorageKeyEncodeError::AccountOwnerTooLarge { len, max }
233            }
234        }
235    }
236
237    const fn from_principal_encode_error(
238        err: crate::types::PrincipalEncodeError,
239    ) -> StorageKeyEncodeError {
240        match err {
241            crate::types::PrincipalEncodeError::TooLarge { len, max } => {
242                StorageKeyEncodeError::PrincipalTooLarge { len, max }
243            }
244        }
245    }
246
247    /// Encode this key into its fixed-size on-disk representation.
248    pub fn to_bytes(self) -> Result<[u8; Self::STORED_SIZE_USIZE], StorageKeyEncodeError> {
249        // Phase 1: write variant tag and select fixed payload window.
250        let mut buf = [0u8; Self::STORED_SIZE_USIZE];
251        buf[Self::TAG_OFFSET] = self.tag();
252        let payload = &mut buf[Self::PAYLOAD_OFFSET..=Self::PAYLOAD_SIZE];
253
254        // Phase 2: encode variant payload into the normalized fixed-width frame.
255        match self {
256            Self::Account(v) => {
257                let bytes = v.to_bytes().map_err(Self::from_account_encode_error)?;
258                if bytes.len() != Self::ACCOUNT_MAX_SIZE {
259                    return Err(StorageKeyEncodeError::AccountLengthMismatch {
260                        len: bytes.len(),
261                        expected: Self::ACCOUNT_MAX_SIZE,
262                    });
263                }
264                payload[..bytes.len()].copy_from_slice(&bytes);
265            }
266            Self::Int(v) => {
267                let biased = v.cast_unsigned() ^ (1u64 << 63);
268                payload[..Self::INT_SIZE].copy_from_slice(&biased.to_be_bytes());
269            }
270            Self::Uint(v) => payload[..Self::UINT_SIZE].copy_from_slice(&v.to_be_bytes()),
271            Self::Timestamp(v) => {
272                payload[..Self::TIMESTAMP_SIZE].copy_from_slice(&v.repr().to_be_bytes());
273            }
274            Self::Principal(v) => {
275                let bytes = v.to_bytes().map_err(Self::from_principal_encode_error)?;
276                let len = bytes.len();
277                payload[0] =
278                    u8::try_from(len).map_err(|_| StorageKeyEncodeError::PrincipalTooLarge {
279                        len,
280                        max: Principal::MAX_LENGTH_IN_BYTES as usize,
281                    })?;
282                payload[1..=len].copy_from_slice(&bytes[..len]);
283            }
284            Self::Subaccount(v) => payload[..Self::SUBACCOUNT_SIZE].copy_from_slice(&v.to_array()),
285            Self::Ulid(v) => payload[..Self::ULID_SIZE].copy_from_slice(&v.to_bytes()),
286            Self::Unit => {}
287        }
288
289        Ok(buf)
290    }
291
292    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, StorageKeyDecodeError> {
293        let bytes: &[u8; Self::STORED_SIZE_USIZE] = bytes
294            .try_into()
295            .map_err(|_| StorageKeyDecodeError::InvalidSize)?;
296
297        Self::try_from_stored_bytes(bytes)
298    }
299
300    /// Decode one storage key from one already size-validated stored frame.
301    pub(crate) fn try_from_stored_bytes(
302        bytes: &[u8; Self::STORED_SIZE_USIZE],
303    ) -> Result<Self, StorageKeyDecodeError> {
304        let tag = bytes[Self::TAG_OFFSET];
305        let payload = &bytes[Self::PAYLOAD_OFFSET..=Self::PAYLOAD_SIZE];
306
307        let ensure_zero_padding = |used: usize, ctx: &'static str| {
308            if payload[used..].iter().all(|&b| b == 0) {
309                Ok(())
310            } else {
311                Err(StorageKeyDecodeError::NonZeroPadding { field: ctx })
312            }
313        };
314
315        // Phase 2: decode tagged payload and enforce zero-padding invariants.
316        match tag {
317            Self::TAG_ACCOUNT => {
318                let end = Account::STORED_SIZE as usize;
319                ensure_zero_padding(end, "account")?;
320                Ok(Self::Account(
321                    Account::try_from_bytes(&payload[..end]).map_err(|reason| {
322                        StorageKeyDecodeError::InvalidAccountPayload { reason }
323                    })?,
324                ))
325            }
326            Self::TAG_INT => {
327                let mut buf = [0u8; Self::INT_SIZE];
328                buf.copy_from_slice(&payload[..Self::INT_SIZE]);
329                ensure_zero_padding(Self::INT_SIZE, "int")?;
330                Ok(Self::Int(
331                    (u64::from_be_bytes(buf) ^ (1u64 << 63)).cast_signed(),
332                ))
333            }
334            Self::TAG_PRINCIPAL => {
335                let len = payload[0] as usize;
336                if len > Principal::MAX_LENGTH_IN_BYTES as usize {
337                    return Err(StorageKeyDecodeError::InvalidPrincipalLength);
338                }
339                ensure_zero_padding(1 + len, "principal")?;
340                Ok(Self::Principal(Principal::from_slice(&payload[1..=len])))
341            }
342            Self::TAG_SUBACCOUNT => {
343                ensure_zero_padding(Self::SUBACCOUNT_SIZE, "subaccount")?;
344                let mut buf = [0u8; Self::SUBACCOUNT_SIZE];
345                buf.copy_from_slice(&payload[..Self::SUBACCOUNT_SIZE]);
346                Ok(Self::Subaccount(Subaccount::from_array(buf)))
347            }
348            Self::TAG_TIMESTAMP => {
349                ensure_zero_padding(Self::TIMESTAMP_SIZE, "timestamp")?;
350                let mut buf = [0u8; Self::TIMESTAMP_SIZE];
351                buf.copy_from_slice(&payload[..Self::TIMESTAMP_SIZE]);
352                Ok(Self::Timestamp(Timestamp::from_repr(i64::from_be_bytes(
353                    buf,
354                ))))
355            }
356            Self::TAG_UINT => {
357                ensure_zero_padding(Self::UINT_SIZE, "uint")?;
358                let mut buf = [0u8; Self::UINT_SIZE];
359                buf.copy_from_slice(&payload[..Self::UINT_SIZE]);
360                Ok(Self::Uint(u64::from_be_bytes(buf)))
361            }
362            Self::TAG_ULID => {
363                ensure_zero_padding(Self::ULID_SIZE, "ulid")?;
364                let mut buf = [0u8; Self::ULID_SIZE];
365                buf.copy_from_slice(&payload[..Self::ULID_SIZE]);
366                Ok(Self::Ulid(Ulid::from_bytes(buf)))
367            }
368            Self::TAG_UNIT => {
369                ensure_zero_padding(0, "unit")?;
370                Ok(Self::Unit)
371            }
372            _ => Err(StorageKeyDecodeError::InvalidTag),
373        }
374    }
375
376    /// Convert this storage-normalized key into a semantic Value.
377    ///
378    /// Intended ONLY for diagnostics, explain output, planner invariants,
379    /// and fingerprinting. Must not be used for query semantics.
380    #[must_use]
381    pub const fn as_value(&self) -> Value {
382        match self {
383            Self::Account(v) => Value::Account(*v),
384            Self::Int(v) => Value::Int(*v),
385            Self::Principal(v) => Value::Principal(*v),
386            Self::Subaccount(v) => Value::Subaccount(*v),
387            Self::Timestamp(v) => Value::Timestamp(*v),
388            Self::Uint(v) => Value::Uint(*v),
389            Self::Ulid(v) => Value::Ulid(*v),
390            Self::Unit => Value::Unit,
391        }
392    }
393}
394
395impl Ord for StorageKey {
396    fn cmp(&self, other: &Self) -> Ordering {
397        match (self, other) {
398            (Self::Account(a), Self::Account(b)) => a.cmp(b),
399            (Self::Int(a), Self::Int(b)) => a.cmp(b),
400            (Self::Principal(a), Self::Principal(b)) => a.cmp(b),
401            (Self::Uint(a), Self::Uint(b)) => a.cmp(b),
402            (Self::Ulid(a), Self::Ulid(b)) => a.cmp(b),
403            (Self::Subaccount(a), Self::Subaccount(b)) => a.cmp(b),
404            (Self::Timestamp(a), Self::Timestamp(b)) => a.cmp(b),
405            _ => self.variant_rank().cmp(&other.variant_rank()),
406        }
407    }
408}
409
410impl PartialOrd for StorageKey {
411    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
412        Some(self.cmp(other))
413    }
414}
415
416impl TryFrom<&[u8]> for StorageKey {
417    type Error = StorageKeyDecodeError;
418    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
419        Self::try_from_bytes(bytes)
420    }
421}
422
423//
424// TESTS
425//
426
427#[cfg(test)]
428mod tests {
429    use super::{StorageKey, StorageKeyDecodeError, StorageKeyEncodeError};
430    use crate::{
431        types::{
432            Account, Date, Decimal, Duration, Float32, Float64, Int, Int128, Nat, Nat128,
433            Principal, Subaccount, Timestamp, Ulid,
434        },
435        value::{Value, ValueEnum},
436    };
437
438    macro_rules! sample_value_for_scalar {
439        (Account) => {
440            Value::Account(Account::dummy(7))
441        };
442        (Blob) => {
443            Value::Blob(vec![1u8, 2u8, 3u8])
444        };
445        (Bool) => {
446            Value::Bool(true)
447        };
448        (Date) => {
449            Value::Date(Date::new(2024, 1, 2))
450        };
451        (Decimal) => {
452            Value::Decimal(Decimal::new(123, 2))
453        };
454        (Duration) => {
455            Value::Duration(Duration::from_secs(1))
456        };
457        (Enum) => {
458            Value::Enum(ValueEnum::loose("example"))
459        };
460        (Float32) => {
461            Value::Float32(Float32::try_new(1.25).expect("Float32 sample should be finite"))
462        };
463        (Float64) => {
464            Value::Float64(Float64::try_new(2.5).expect("Float64 sample should be finite"))
465        };
466        (Int) => {
467            Value::Int(-7)
468        };
469        (Int128) => {
470            Value::Int128(Int128::from(123i128))
471        };
472        (IntBig) => {
473            Value::IntBig(Int::from(99i32))
474        };
475        (Principal) => {
476            Value::Principal(Principal::from_slice(&[1u8, 2u8, 3u8]))
477        };
478        (Subaccount) => {
479            Value::Subaccount(Subaccount::new([1u8; 32]))
480        };
481        (Text) => {
482            Value::Text("example".to_string())
483        };
484        (Timestamp) => {
485            Value::Timestamp(Timestamp::from_secs(1))
486        };
487        (Uint) => {
488            Value::Uint(7)
489        };
490        (Uint128) => {
491            Value::Uint128(Nat128::from(9u128))
492        };
493        (UintBig) => {
494            Value::UintBig(Nat::from(11u64))
495        };
496        (Ulid) => {
497            Value::Ulid(Ulid::from_u128(42))
498        };
499        (Unit) => {
500            Value::Unit
501        };
502    }
503
504    fn registry_storage_encodable_cases() -> Vec<(Value, bool)> {
505        macro_rules! collect_cases {
506            ( @entries $( ($scalar:ident, $family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
507                vec![ $( (sample_value_for_scalar!($scalar), $is_storage_key_encodable) ),* ]
508            };
509            ( @args $($ignore:tt)*; @entries $( ($scalar:ident, $family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
510                vec![ $( (sample_value_for_scalar!($scalar), $is_storage_key_encodable) ),* ]
511            };
512        }
513
514        scalar_registry!(collect_cases)
515    }
516
517    #[test]
518    fn storage_key_try_from_value_matches_registry_flag() {
519        for (value, expected_encodable) in registry_storage_encodable_cases() {
520            assert_eq!(
521                StorageKey::try_from_value(&value).is_ok(),
522                expected_encodable,
523                "value: {value:?}"
524            );
525        }
526    }
527
528    #[test]
529    fn storage_key_known_encodability_contracts() {
530        assert!(StorageKey::try_from_value(&Value::Unit).is_ok());
531        assert!(StorageKey::try_from_value(&Value::Decimal(Decimal::new(1, 0))).is_err());
532        assert!(StorageKey::try_from_value(&Value::Text("x".to_string())).is_err());
533        assert!(StorageKey::try_from_value(&Value::Account(Account::dummy(1))).is_ok());
534    }
535
536    #[test]
537    fn storage_key_unsupported_values_report_kind() {
538        let decimal_err = StorageKey::try_from_value(&Value::Decimal(Decimal::new(1, 0)))
539            .expect_err("Decimal is not storage-key encodable");
540        assert!(matches!(
541            decimal_err,
542            StorageKeyEncodeError::UnsupportedValueKind { kind } if kind == "Decimal"
543        ));
544
545        let text_err = StorageKey::try_from_value(&Value::Text("x".to_string()))
546            .expect_err("Text is not storage-key encodable");
547        assert!(matches!(
548            text_err,
549            StorageKeyEncodeError::UnsupportedValueKind { kind } if kind == "Text"
550        ));
551    }
552
553    #[test]
554    fn storage_keys_sort_deterministically_across_mixed_variants() {
555        let mut keys = vec![
556            StorageKey::try_from_value(&Value::Unit).expect("Unit is encodable"),
557            StorageKey::try_from_value(&Value::Ulid(Ulid::from_u128(2)))
558                .expect("Ulid is encodable"),
559            StorageKey::try_from_value(&Value::Uint(2)).expect("Uint is encodable"),
560            StorageKey::try_from_value(&Value::Timestamp(Timestamp::from_secs(2)))
561                .expect("Timestamp is encodable"),
562            StorageKey::try_from_value(&Value::Subaccount(Subaccount::new([3u8; 32])))
563                .expect("Subaccount is encodable"),
564            StorageKey::try_from_value(&Value::Principal(Principal::from_slice(&[9u8])))
565                .expect("Principal is encodable"),
566            StorageKey::try_from_value(&Value::Int(-1)).expect("Int is encodable"),
567            StorageKey::try_from_value(&Value::Account(Account::dummy(3)))
568                .expect("Account is encodable"),
569        ];
570
571        keys.sort();
572
573        let expected = vec![
574            StorageKey::Account(Account::dummy(3)),
575            StorageKey::Int(-1),
576            StorageKey::Principal(Principal::from_slice(&[9u8])),
577            StorageKey::Subaccount(Subaccount::new([3u8; 32])),
578            StorageKey::Timestamp(Timestamp::from_secs(2)),
579            StorageKey::Uint(2),
580            StorageKey::Ulid(Ulid::from_u128(2)),
581            StorageKey::Unit,
582        ];
583
584        assert_eq!(keys, expected);
585    }
586
587    #[test]
588    fn storage_key_decode_rejects_invalid_size_as_structured_error() {
589        let err =
590            StorageKey::try_from_bytes(&[]).expect_err("decode should reject invalid key size");
591        assert!(matches!(err, StorageKeyDecodeError::InvalidSize));
592    }
593
594    #[test]
595    fn storage_key_decode_rejects_invalid_tag_as_structured_error() {
596        let mut bytes = [0u8; StorageKey::STORED_SIZE_USIZE];
597        bytes[StorageKey::TAG_OFFSET] = 0xFF;
598
599        let err = StorageKey::try_from_bytes(&bytes).expect_err("decode should reject invalid tag");
600        assert!(matches!(err, StorageKeyDecodeError::InvalidTag));
601    }
602
603    #[test]
604    fn storage_key_decode_rejects_non_zero_padding_with_segment_context() {
605        let mut bytes = [0u8; StorageKey::STORED_SIZE_USIZE];
606        bytes[StorageKey::TAG_OFFSET] = StorageKey::TAG_UNIT;
607        bytes[StorageKey::PAYLOAD_OFFSET] = 1;
608
609        let err = StorageKey::try_from_bytes(&bytes)
610            .expect_err("decode should reject non-zero padding for unit payload");
611        assert!(matches!(
612            err,
613            StorageKeyDecodeError::NonZeroPadding { field } if field == "unit"
614        ));
615    }
616}