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