monero_epee/stack.rs
1//! A non-allocating alternative to `Vec`, used to track the state of an EPEE decoder.
2//!
3//! This is only possible due to bounding the depth of objects we decode, which we are able to do
4//! with complete correctness due to EPEE defining a maximum depth of just `100`. This limit is
5//! also sufficiently small as to make this not only feasible yet performant, with the stack taking
6//! less than a kibibyte (even on 64-bit platforms).
7//!
8//! This code is internal to `monero-epee` yet is still written not to panic in any case.
9
10/*
11 Introducing `NonZero` bumped us from a MSRV of Rust 1.60 to Rust 1.79. We have no incentive to
12 support Rust 1.60 here, yet it would be trivial to expand the supported versions of Rust.
13*/
14use core::num::NonZero;
15
16use crate::{EpeeError, Type, TypeOrEntry};
17
18// https://github.com/monero-project/monero/blob/8d4c625713e3419573dfcc7119c8848f47cabbaa/
19// contrib/epee/include/storages/portable_storage_from_bin.h#L42
20const EPEE_LIB_MAX_OBJECT_DEPTH: usize = 100;
21/*
22 Explicitly set a larger depth in case we have slight differences in counting the current depth.
23
24 The goal of this library is to decode at _least_ the set of objects EPEE itself will handle.
25 While we may be slightly more tolerant, this is accepted over incompatibility given the lack of a
26 strict specification for EPEE.
27
28 Additionally, decoding a larget set of objects will not be considered an incompatibility so long
29 as encodings (unsupported at the time of writing this comment) will always be handled by EPEE,
30 ensuring _mutual_ compatibility.
31*/
32const MAX_OBJECT_DEPTH: usize = EPEE_LIB_MAX_OBJECT_DEPTH + 2;
33
34/// An array of `TypeOrEntry`, using `u4` for each value.
35struct PackedTypes([u8; MAX_OBJECT_DEPTH.div_ceil(2)]);
36impl PackedTypes {
37 fn get(&self, i: usize) -> TypeOrEntry {
38 let mut entry = self.0[i / 2];
39 entry >>= (i & 1) * 4;
40 entry &= 0b1111;
41 match entry {
42 0 => TypeOrEntry::Entry,
43 1 => TypeOrEntry::Type(Type::Int64),
44 2 => TypeOrEntry::Type(Type::Int32),
45 3 => TypeOrEntry::Type(Type::Int16),
46 4 => TypeOrEntry::Type(Type::Int8),
47 5 => TypeOrEntry::Type(Type::Uint64),
48 6 => TypeOrEntry::Type(Type::Uint32),
49 7 => TypeOrEntry::Type(Type::Uint16),
50 8 => TypeOrEntry::Type(Type::Uint8),
51 9 => TypeOrEntry::Type(Type::Double),
52 10 => TypeOrEntry::Type(Type::String),
53 11 => TypeOrEntry::Type(Type::Bool),
54 12 => TypeOrEntry::Type(Type::Object),
55 13 ..= 15 => panic!("`PackedTypes` was written to with a non-existent `TypeOrEntry`"),
56 _ => unreachable!("masked by 0b1111"),
57 }
58 }
59
60 fn set(&mut self, i: usize, kind: TypeOrEntry) {
61 let four_bits = match kind {
62 TypeOrEntry::Entry => 0,
63 #[expect(clippy::as_conversions)]
64 TypeOrEntry::Type(kind) => kind as u8,
65 };
66 let shift = (i & 1) * 4;
67 // Clear the existing value in this slot
68 self.0[i / 2] &= 0b1111_0000 >> shift;
69 // Set the new value
70 self.0[i / 2] |= four_bits << shift;
71 }
72}
73
74/// A non-allocating `Vec`.
75///
76/// This has a maximum depth premised on the bound for an EPEE's object depth.
77pub(crate) struct Stack {
78 /*
79 We represent items to decode as `(TypeOrEntry, usize)` so that if told to decode a vector with
80 one billion entries, we don't have to allocate
81 `vec![TypeOrEntry::Entry(Type::*), 1_000_000_000]` to keep track of the state. Instead, the
82 size of the state is solely a function of depth, not width.
83
84 The following two arrays are separate as Rust would pad `(TypeOrEntry, usize)` to 16 bytes,
85 when it only requires 9 bytes to represent. Additionally, as there's less than 2**4 possible
86 states for a `TypeOrEntry`, we represent it with a `u4` (via `PackedTypes`).
87 */
88 /// The type of the item being read.
89 types: PackedTypes,
90 /// The amount remaining for the item being read.
91 amounts: [NonZero<usize>; MAX_OBJECT_DEPTH],
92
93 /// The current depth of the stack.
94 ///
95 /// This is analogous to the length of a `Vec`, yet we use the term `depth` to distinguish how it
96 /// tracks the depth of an object, not the amount of items present (which would be a function of
97 /// depth and width, as noted above).
98 depth: u8,
99}
100
101/*
102 Because every time we decode an object, we allocate this fixed-size item on the stack (avoiding
103 requiring dynamic allocation on the heap), ensure it's tiny and not an issue to allocate.
104*/
105#[cfg(test)]
106const _ASSERT_KIBIBYTE_STACK: [(); 1024 - core::mem::size_of::<Stack>()] =
107 [(); 1024 - core::mem::size_of::<Stack>()];
108
109impl Stack {
110 /// Create a new stack to use with decoding the root object.
111 #[inline(always)]
112 pub(crate) fn root_object() -> Self {
113 /*
114 Zero-initialize the arrays.
115
116 Because we require `amounts` to be non-zero, we use `NonZero::MIN`. Alternatively, we'd need
117 to use `unsafe` for the uninitialized memory.
118 */
119 let mut types = PackedTypes([0; MAX_OBJECT_DEPTH.div_ceil(2)]);
120 let mut amounts = [NonZero::<usize>::MIN; MAX_OBJECT_DEPTH];
121
122 types.set(0, TypeOrEntry::Type(Type::Object));
123 amounts[0] = NonZero::<usize>::MIN; // 1
124
125 Self { types, amounts, depth: 1 }
126 }
127
128 /// The current stack depth.
129 #[inline(always)]
130 pub(crate) fn depth(&self) -> usize {
131 usize::from(self.depth)
132 }
133
134 /// Peek the current item on the stack.
135 #[inline(always)]
136 pub(crate) fn peek(&self) -> Option<(TypeOrEntry, NonZero<usize>)> {
137 let i = self.depth().checked_sub(1)?;
138 Some((self.types.get(i), self.amounts[i]))
139 }
140
141 /// Pop the next item from the stack.
142 pub(crate) fn pop(&mut self) -> Option<TypeOrEntry> {
143 let i = self.depth().checked_sub(1)?;
144
145 let kind = self.types.get(i);
146
147 // This will not panic as `amount` is unsigned and non-zero.
148 let amount = self.amounts[i].get() - 1;
149 if let Some(amount) = NonZero::new(amount) {
150 self.amounts[i] = amount;
151 } else {
152 // This will not panic as we know depth can have `1` subtracted.
153 self.depth -= 1;
154 }
155
156 Some(kind)
157 }
158
159 /// Push an item onto the stack.
160 pub(crate) fn push(&mut self, kind: TypeOrEntry, amount: usize) -> Result<(), EpeeError> {
161 // Assert the maximum depth for an object
162 if self.depth() == MAX_OBJECT_DEPTH {
163 Err(EpeeError::DepthLimitExceeded)?;
164 }
165
166 let Some(amount) = NonZero::new(amount) else {
167 // If we have nothing to decode, immediately return
168 return Ok(());
169 };
170
171 // These will not panic due to our depth check at the start of the function
172 self.types.set(self.depth(), kind);
173 self.amounts[self.depth()] = amount;
174 self.depth += 1;
175
176 Ok(())
177 }
178}