1#![expect(clippy::cast_possible_truncation)]
10
11use crate::{
12 error::InternalError,
13 traits::Repr,
14 types::{Account, Principal, Subaccount, Timestamp, Ulid},
15};
16use candid::CandidType;
17use serde::Deserialize;
18use std::cmp::Ordering;
19use thiserror::Error as ThisError;
20
21#[derive(Debug, ThisError)]
27pub enum StorageKeyEncodeError {
28 #[error("account owner principal exceeds max length: {len} bytes (limit {max})")]
29 AccountOwnerTooLarge { len: usize, max: usize },
30
31 #[error("value kind '{kind}' is not storage-key encodable")]
32 UnsupportedValueKind { kind: &'static str },
33
34 #[error("principal exceeds max length: {len} bytes (limit {max})")]
35 PrincipalTooLarge { len: usize, max: usize },
36}
37
38impl From<StorageKeyEncodeError> for InternalError {
39 fn from(err: StorageKeyEncodeError) -> Self {
40 Self::serialize_unsupported(err.to_string())
41 }
42}
43
44#[derive(Debug, ThisError)]
50pub enum StorageKeyDecodeError {
51 #[error("corrupted StorageKey: invalid size")]
52 InvalidSize,
53
54 #[error("corrupted StorageKey: invalid tag")]
55 InvalidTag,
56
57 #[error("corrupted StorageKey: invalid principal length")]
58 InvalidPrincipalLength,
59
60 #[error("corrupted StorageKey: non-zero {field} padding")]
61 NonZeroPadding { field: &'static str },
62
63 #[error("corrupted StorageKey: invalid account payload ({reason})")]
64 InvalidAccountPayload { reason: &'static str },
65}
66
67#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
78pub enum StorageKey {
79 Account(Account),
80 Int(i64),
81 Principal(Principal),
82 Subaccount(Subaccount),
83 Timestamp(Timestamp),
84 Nat(u64),
85 Ulid(Ulid),
86 Unit,
87}
88
89impl StorageKey {
90 pub(crate) const TAG_ACCOUNT: u8 = 0;
92 pub(crate) const TAG_INT: u8 = 1;
93 pub(crate) const TAG_PRINCIPAL: u8 = 2;
94 pub(crate) const TAG_SUBACCOUNT: u8 = 3;
95 pub(crate) const TAG_TIMESTAMP: u8 = 4;
96 pub(crate) const TAG_NAT: u8 = 5;
97 pub(crate) const TAG_ULID: u8 = 6;
98 pub(crate) const TAG_UNIT: u8 = 7;
99
100 pub const STORED_SIZE_BYTES: u64 = 64;
103 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
104
105 const TAG_SIZE: usize = 1;
106 pub(crate) const TAG_OFFSET: usize = 0;
107
108 pub(crate) const PAYLOAD_OFFSET: usize = Self::TAG_SIZE;
109 const PAYLOAD_SIZE: usize = Self::STORED_SIZE_USIZE - Self::TAG_SIZE;
110
111 pub(crate) const INT_SIZE: usize = 8;
112 pub(crate) const NAT_SIZE: usize = 8;
113 pub(crate) const TIMESTAMP_SIZE: usize = 8;
114 pub(crate) const ULID_SIZE: usize = 16;
115 pub(crate) const SUBACCOUNT_SIZE: usize = 32;
116 const ACCOUNT_MAX_SIZE: usize = 62;
117
118 const fn tag(&self) -> u8 {
119 match self {
120 Self::Account(_) => Self::TAG_ACCOUNT,
121 Self::Int(_) => Self::TAG_INT,
122 Self::Principal(_) => Self::TAG_PRINCIPAL,
123 Self::Subaccount(_) => Self::TAG_SUBACCOUNT,
124 Self::Timestamp(_) => Self::TAG_TIMESTAMP,
125 Self::Nat(_) => Self::TAG_NAT,
126 Self::Ulid(_) => Self::TAG_ULID,
127 Self::Unit => Self::TAG_UNIT,
128 }
129 }
130
131 pub const MIN: Self = Self::Account(Account::storage_min_sentinel());
133
134 #[must_use]
135 pub const fn lower_bound() -> Self {
136 Self::MIN
137 }
138
139 #[must_use]
140 pub const fn upper_bound() -> Self {
141 Self::Unit
142 }
143
144 const fn variant_rank(&self) -> u8 {
145 self.tag()
146 }
147
148 const fn from_account_encode_error(
149 err: crate::types::AccountEncodeError,
150 ) -> StorageKeyEncodeError {
151 match err {
152 crate::types::AccountEncodeError::OwnerEncode(inner) => {
153 Self::from_principal_encode_error(inner)
154 }
155 crate::types::AccountEncodeError::OwnerTooLarge { len, max } => {
156 StorageKeyEncodeError::AccountOwnerTooLarge { len, max }
157 }
158 }
159 }
160
161 const fn from_principal_encode_error(
162 err: crate::types::PrincipalEncodeError,
163 ) -> StorageKeyEncodeError {
164 match err {
165 crate::types::PrincipalEncodeError::TooLarge { len, max } => {
166 StorageKeyEncodeError::PrincipalTooLarge { len, max }
167 }
168 }
169 }
170
171 pub fn to_bytes(self) -> Result<[u8; Self::STORED_SIZE_USIZE], StorageKeyEncodeError> {
173 let mut buf = [0u8; Self::STORED_SIZE_USIZE];
175 buf[Self::TAG_OFFSET] = self.tag();
176 let payload = &mut buf[Self::PAYLOAD_OFFSET..=Self::PAYLOAD_SIZE];
177
178 match self {
180 Self::Account(v) => {
181 let bytes = v
182 .to_stored_bytes()
183 .map_err(Self::from_account_encode_error)?;
184 payload[..Self::ACCOUNT_MAX_SIZE].copy_from_slice(&bytes);
185 }
186 Self::Int(v) => {
187 let biased = v.cast_unsigned() ^ (1u64 << 63);
188 payload[..Self::INT_SIZE].copy_from_slice(&biased.to_be_bytes());
189 }
190 Self::Nat(v) => payload[..Self::NAT_SIZE].copy_from_slice(&v.to_be_bytes()),
191 Self::Timestamp(v) => {
192 payload[..Self::TIMESTAMP_SIZE].copy_from_slice(&v.repr().to_be_bytes());
193 }
194 Self::Principal(v) => {
195 let bytes = v
196 .stored_bytes()
197 .map_err(Self::from_principal_encode_error)?;
198 let len = bytes.len();
199 payload[0] =
200 u8::try_from(len).map_err(|_| StorageKeyEncodeError::PrincipalTooLarge {
201 len,
202 max: Principal::MAX_LENGTH_IN_BYTES as usize,
203 })?;
204 payload[1..=len].copy_from_slice(bytes);
205 }
206 Self::Subaccount(v) => payload[..Self::SUBACCOUNT_SIZE].copy_from_slice(&v.to_array()),
207 Self::Ulid(v) => payload[..Self::ULID_SIZE].copy_from_slice(&v.to_bytes()),
208 Self::Unit => {}
209 }
210
211 Ok(buf)
212 }
213
214 pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, StorageKeyDecodeError> {
215 let bytes: &[u8; Self::STORED_SIZE_USIZE] = bytes
216 .try_into()
217 .map_err(|_| StorageKeyDecodeError::InvalidSize)?;
218
219 Self::try_from_stored_bytes(bytes)
220 }
221
222 pub(crate) fn try_from_stored_bytes(
224 bytes: &[u8; Self::STORED_SIZE_USIZE],
225 ) -> Result<Self, StorageKeyDecodeError> {
226 let tag = bytes[Self::TAG_OFFSET];
227 let payload = &bytes[Self::PAYLOAD_OFFSET..=Self::PAYLOAD_SIZE];
228
229 let ensure_zero_padding = |used: usize, ctx: &'static str| {
230 if payload[used..].iter().all(|&b| b == 0) {
231 Ok(())
232 } else {
233 Err(StorageKeyDecodeError::NonZeroPadding { field: ctx })
234 }
235 };
236
237 match tag {
239 Self::TAG_ACCOUNT => {
240 let end = Account::STORED_SIZE as usize;
241 ensure_zero_padding(end, "account")?;
242 Ok(Self::Account(
243 Account::try_from_bytes(&payload[..end]).map_err(|reason| {
244 StorageKeyDecodeError::InvalidAccountPayload { reason }
245 })?,
246 ))
247 }
248 Self::TAG_INT => {
249 let mut buf = [0u8; Self::INT_SIZE];
250 buf.copy_from_slice(&payload[..Self::INT_SIZE]);
251 ensure_zero_padding(Self::INT_SIZE, "int")?;
252 Ok(Self::Int(
253 (u64::from_be_bytes(buf) ^ (1u64 << 63)).cast_signed(),
254 ))
255 }
256 Self::TAG_PRINCIPAL => {
257 let len = payload[0] as usize;
258 if len > Principal::MAX_LENGTH_IN_BYTES as usize {
259 return Err(StorageKeyDecodeError::InvalidPrincipalLength);
260 }
261 ensure_zero_padding(1 + len, "principal")?;
262 Ok(Self::Principal(Principal::from_slice(&payload[1..=len])))
263 }
264 Self::TAG_SUBACCOUNT => {
265 ensure_zero_padding(Self::SUBACCOUNT_SIZE, "subaccount")?;
266 let mut buf = [0u8; Self::SUBACCOUNT_SIZE];
267 buf.copy_from_slice(&payload[..Self::SUBACCOUNT_SIZE]);
268 Ok(Self::Subaccount(Subaccount::from_array(buf)))
269 }
270 Self::TAG_TIMESTAMP => {
271 ensure_zero_padding(Self::TIMESTAMP_SIZE, "timestamp")?;
272 let mut buf = [0u8; Self::TIMESTAMP_SIZE];
273 buf.copy_from_slice(&payload[..Self::TIMESTAMP_SIZE]);
274 Ok(Self::Timestamp(Timestamp::from_repr(i64::from_be_bytes(
275 buf,
276 ))))
277 }
278 Self::TAG_NAT => {
279 ensure_zero_padding(Self::NAT_SIZE, "nat")?;
280 let mut buf = [0u8; Self::NAT_SIZE];
281 buf.copy_from_slice(&payload[..Self::NAT_SIZE]);
282 Ok(Self::Nat(u64::from_be_bytes(buf)))
283 }
284 Self::TAG_ULID => {
285 ensure_zero_padding(Self::ULID_SIZE, "ulid")?;
286 let mut buf = [0u8; Self::ULID_SIZE];
287 buf.copy_from_slice(&payload[..Self::ULID_SIZE]);
288 Ok(Self::Ulid(Ulid::from_bytes(buf)))
289 }
290 Self::TAG_UNIT => {
291 ensure_zero_padding(0, "unit")?;
292 Ok(Self::Unit)
293 }
294 _ => Err(StorageKeyDecodeError::InvalidTag),
295 }
296 }
297}
298
299impl Ord for StorageKey {
300 fn cmp(&self, other: &Self) -> Ordering {
301 match (self, other) {
302 (Self::Account(a), Self::Account(b)) => a.cmp(b),
303 (Self::Int(a), Self::Int(b)) => a.cmp(b),
304 (Self::Principal(a), Self::Principal(b)) => a.cmp(b),
305 (Self::Nat(a), Self::Nat(b)) => a.cmp(b),
306 (Self::Ulid(a), Self::Ulid(b)) => a.cmp(b),
307 (Self::Subaccount(a), Self::Subaccount(b)) => a.cmp(b),
308 (Self::Timestamp(a), Self::Timestamp(b)) => a.cmp(b),
309 _ => self.variant_rank().cmp(&other.variant_rank()),
310 }
311 }
312}
313
314impl PartialOrd for StorageKey {
315 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
316 Some(self.cmp(other))
317 }
318}
319
320impl TryFrom<&[u8]> for StorageKey {
321 type Error = StorageKeyDecodeError;
322 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
323 Self::try_from_bytes(bytes)
324 }
325}
326
327#[cfg(test)]
332mod tests {
333 use super::{StorageKey, StorageKeyDecodeError, StorageKeyEncodeError};
334 use crate::{
335 types::{
336 Account, Date, Decimal, Duration, Float32, Float64, IntBig, NatBig, Principal,
337 Subaccount, Timestamp, Ulid,
338 },
339 value::{Value, ValueEnum, storage_key_from_runtime_value},
340 };
341
342 macro_rules! sample_value_for_scalar {
343 (Account) => {
344 Value::Account(Account::from_parts(
345 Principal::from_slice(&[7]),
346 Some(Subaccount::from_array([7; 32])),
347 ))
348 };
349 (Blob) => {
350 Value::Blob(vec![1u8, 2u8, 3u8])
351 };
352 (Bool) => {
353 Value::Bool(true)
354 };
355 (Date) => {
356 Value::Date(Date::new(2024, 1, 2))
357 };
358 (Decimal) => {
359 Value::Decimal(Decimal::new(123, 2))
360 };
361 (Duration) => {
362 Value::Duration(Duration::from_secs(1))
363 };
364 (Enum) => {
365 Value::Enum(ValueEnum::loose("example"))
366 };
367 (Float32) => {
368 Value::Float32(Float32::try_new(1.25).expect("Float32 sample should be finite"))
369 };
370 (Float64) => {
371 Value::Float64(Float64::try_new(2.5).expect("Float64 sample should be finite"))
372 };
373 (Int) => {
374 Value::Int64(-7)
375 };
376 (Int128) => {
377 Value::Int128(123i128)
378 };
379 (IntBig) => {
380 Value::IntBig(IntBig::from(99i32))
381 };
382 (Principal) => {
383 Value::Principal(Principal::from_slice(&[1u8, 2u8, 3u8]))
384 };
385 (Subaccount) => {
386 Value::Subaccount(Subaccount::new([1u8; 32]))
387 };
388 (Text) => {
389 Value::Text("example".to_string())
390 };
391 (Timestamp) => {
392 Value::Timestamp(Timestamp::from_secs(1))
393 };
394 (Nat) => {
395 Value::Nat64(7)
396 };
397 (Nat128) => {
398 Value::Nat128(9u128)
399 };
400 (NatBig) => {
401 Value::NatBig(NatBig::from(11u64))
402 };
403 (Ulid) => {
404 Value::Ulid(Ulid::from_u128(42))
405 };
406 (Unit) => {
407 Value::Unit
408 };
409 }
410
411 fn registry_storage_encodable_cases() -> Vec<(Value, bool)> {
412 macro_rules! collect_cases {
413 ( @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) ),* $(,)? ) => {
414 vec![ $( (sample_value_for_scalar!($scalar), $is_storage_key_encodable) ),* ]
415 };
416 ( @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) ),* $(,)? ) => {
417 vec![ $( (sample_value_for_scalar!($scalar), $is_storage_key_encodable) ),* ]
418 };
419 }
420
421 scalar_registry!(collect_cases)
422 }
423
424 #[test]
425 fn storage_key_try_from_value_matches_registry_flag() {
426 for (value, expected_encodable) in registry_storage_encodable_cases() {
427 assert_eq!(
428 storage_key_from_runtime_value(&value).is_ok(),
429 expected_encodable,
430 "value: {value:?}"
431 );
432 }
433 }
434
435 #[test]
436 fn storage_key_known_encodability_contracts() {
437 assert!(storage_key_from_runtime_value(&Value::Unit).is_ok());
438 assert!(storage_key_from_runtime_value(&Value::Decimal(Decimal::new(1, 0))).is_err());
439 assert!(storage_key_from_runtime_value(&Value::Text("x".to_string())).is_err());
440 assert!(
441 storage_key_from_runtime_value(&Value::Account(Account::from_parts(
442 Principal::from_slice(&[1]),
443 Some(Subaccount::from_array([1; 32]))
444 )))
445 .is_ok()
446 );
447 }
448
449 #[test]
450 fn storage_key_unsupported_values_report_kind() {
451 let decimal_err = storage_key_from_runtime_value(&Value::Decimal(Decimal::new(1, 0)))
452 .expect_err("Decimal is not storage-key encodable");
453 assert!(matches!(
454 decimal_err,
455 StorageKeyEncodeError::UnsupportedValueKind { kind } if kind == "Decimal"
456 ));
457
458 let text_err = storage_key_from_runtime_value(&Value::Text("x".to_string()))
459 .expect_err("Text is not storage-key encodable");
460 assert!(matches!(
461 text_err,
462 StorageKeyEncodeError::UnsupportedValueKind { kind } if kind == "Text"
463 ));
464 }
465
466 #[test]
467 fn storage_keys_sort_deterministically_across_mixed_variants() {
468 let mut keys = vec![
469 storage_key_from_runtime_value(&Value::Unit).expect("Unit is encodable"),
470 storage_key_from_runtime_value(&Value::Ulid(Ulid::from_u128(2)))
471 .expect("Ulid is encodable"),
472 storage_key_from_runtime_value(&Value::Nat64(2)).expect("Nat is encodable"),
473 storage_key_from_runtime_value(&Value::Timestamp(Timestamp::from_secs(2)))
474 .expect("Timestamp is encodable"),
475 storage_key_from_runtime_value(&Value::Subaccount(Subaccount::new([3u8; 32])))
476 .expect("Subaccount is encodable"),
477 storage_key_from_runtime_value(&Value::Principal(Principal::from_slice(&[9u8])))
478 .expect("Principal is encodable"),
479 storage_key_from_runtime_value(&Value::Int64(-1)).expect("Int is encodable"),
480 storage_key_from_runtime_value(&Value::Account(Account::from_parts(
481 Principal::from_slice(&[3]),
482 Some(Subaccount::from_array([3; 32])),
483 )))
484 .expect("Account is encodable"),
485 ];
486
487 keys.sort();
488
489 let expected = vec![
490 StorageKey::Account(Account::from_parts(
491 Principal::from_slice(&[3]),
492 Some(Subaccount::from_array([3; 32])),
493 )),
494 StorageKey::Int(-1),
495 StorageKey::Principal(Principal::from_slice(&[9u8])),
496 StorageKey::Subaccount(Subaccount::new([3u8; 32])),
497 StorageKey::Timestamp(Timestamp::from_secs(2)),
498 StorageKey::Nat(2),
499 StorageKey::Ulid(Ulid::from_u128(2)),
500 StorageKey::Unit,
501 ];
502
503 assert_eq!(keys, expected);
504 }
505
506 #[test]
507 fn storage_key_decode_rejects_invalid_size_as_structured_error() {
508 let err =
509 StorageKey::try_from_bytes(&[]).expect_err("decode should reject invalid key size");
510 assert!(matches!(err, StorageKeyDecodeError::InvalidSize));
511 }
512
513 #[test]
514 fn storage_key_decode_rejects_invalid_tag_as_structured_error() {
515 let mut bytes = [0u8; StorageKey::STORED_SIZE_USIZE];
516 bytes[StorageKey::TAG_OFFSET] = 0xFF;
517
518 let err = StorageKey::try_from_bytes(&bytes).expect_err("decode should reject invalid tag");
519 assert!(matches!(err, StorageKeyDecodeError::InvalidTag));
520 }
521
522 #[test]
523 fn storage_key_decode_rejects_non_zero_padding_with_segment_context() {
524 let mut bytes = [0u8; StorageKey::STORED_SIZE_USIZE];
525 bytes[StorageKey::TAG_OFFSET] = StorageKey::TAG_UNIT;
526 bytes[StorageKey::PAYLOAD_OFFSET] = 1;
527
528 let err = StorageKey::try_from_bytes(&bytes)
529 .expect_err("decode should reject non-zero padding for unit payload");
530 assert!(matches!(
531 err,
532 StorageKeyDecodeError::NonZeroPadding { field } if field == "unit"
533 ));
534 }
535}