Skip to main content

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}