Skip to main content

esp_nvs/
raw.rs

1#[cfg(feature = "debug-logs")]
2use alloc::format;
3use alloc::vec;
4use core::fmt::{
5    Debug,
6    Formatter,
7};
8use core::mem::{
9    size_of,
10    transmute,
11};
12use core::slice::from_raw_parts;
13
14#[cfg(feature = "defmt")]
15use defmt::trace;
16
17use crate::Key;
18use crate::error::Error;
19use crate::internal::{
20    ThinPageHeader,
21    ThinPageState,
22    VersionOffset,
23};
24use crate::platform::{
25    AlignedOps,
26    FnCrc32,
27    Platform,
28};
29use crate::u24::u24;
30
31// -1 is for the leading item of type BLOB_DATA or SZ (for str)
32pub const FLASH_SECTOR_SIZE: usize = 4096;
33pub const ENTRY_STATE_BITMAP_SIZE: usize = 32;
34pub const ENTRIES_PER_PAGE: usize = 126;
35// -1 is for the leading item of type BLOB_DATA or SZ (for str)
36pub const MAX_BLOB_DATA_PER_PAGE: usize = (ENTRIES_PER_PAGE - 1) * size_of::<Item>();
37pub const MAX_BLOB_SIZE: usize = MAX_BLOB_DATA_PER_PAGE * (u8::MAX as usize - VersionOffset::V1 as usize);
38pub const PAGE_HEADER_SIZE: usize = size_of::<PageHeader>();
39pub const ITEM_SIZE: usize = size_of::<Item>();
40
41// Compile-time assertion to ensure page structure size matches flash sector size
42const _: () = assert!(
43    PAGE_HEADER_SIZE + ENTRY_STATE_BITMAP_SIZE + ENTRIES_PER_PAGE * ITEM_SIZE == FLASH_SECTOR_SIZE,
44    "Page structure size must equal flash sector size"
45);
46
47#[repr(C, packed)]
48pub(crate) struct RawPage {
49    pub(crate) header: PageHeader,
50    pub(crate) entry_state_bitmap: [u8; ENTRY_STATE_BITMAP_SIZE],
51    pub(crate) items: Items,
52}
53
54#[repr(C, packed)]
55#[derive(Copy, Clone)]
56pub(crate) union Items {
57    pub(crate) raw: [u8; ENTRIES_PER_PAGE * size_of::<Item>()],
58    pub(crate) entries: [Item; ENTRIES_PER_PAGE],
59}
60
61#[cfg(feature = "debug-logs")]
62impl Debug for Items {
63    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
64        unsafe { self.entries.fmt(f) }
65    }
66}
67
68#[derive(strum::FromRepr, Debug, PartialEq)]
69#[cfg_attr(feature = "defmt", derive(defmt::Format))]
70#[repr(u8)]
71pub(crate) enum EntryMapState {
72    Empty = 0b11,
73    Written = 0b10,
74    Erased = 0b00,
75    Illegal = 0b01,
76}
77
78const PSB_INIT: u32 = 0x1;
79const PSB_FULL: u32 = 0x2;
80const PSB_FREEING: u32 = 0x4;
81const PSB_CORRUPT: u32 = 0x8;
82
83#[derive(strum::FromRepr, strum::Display, Debug, PartialEq, Copy, Clone)]
84#[repr(u32)]
85pub enum PageState {
86    // All bits set, default state after flash erase. Page has not been initialized yet.
87    Uninitialized = u32::MAX,
88
89    // Page is initialized, and will accept writes.
90    Active = PageState::Uninitialized as u32 & !PSB_INIT,
91
92    // Page is marked as full and will not accept new writes.
93    Full = PageState::Active as u32 & !PSB_FULL,
94
95    // Data is being moved from this page to a new one.
96    Freeing = PageState::Full as u32 & !PSB_FREEING,
97
98    // Page was found to be in a corrupt and unrecoverable state.
99    // Instead of being erased immediately, it will be kept for diagnostics and data recovery.
100    // It will be erased once we run out out free pages.
101    Corrupt = PageState::Freeing as u32 & !PSB_CORRUPT,
102
103    // Page object wasn't loaded from flash memory
104    Invalid = 0,
105}
106
107impl From<PageState> for ThinPageState {
108    fn from(val: PageState) -> Self {
109        match val {
110            PageState::Uninitialized => ThinPageState::Uninitialized,
111            PageState::Active => ThinPageState::Active,
112            PageState::Full => ThinPageState::Full,
113            PageState::Freeing => ThinPageState::Freeing,
114            PageState::Corrupt => ThinPageState::Corrupt,
115            PageState::Invalid => ThinPageState::Invalid,
116        }
117    }
118}
119
120const PAGE_STATE_UNINITIALIZED: u32 = PageState::Uninitialized as u32;
121const PAGE_STATE_ACTIVE: u32 = PageState::Active as u32;
122const PAGE_STATE_FULL: u32 = PageState::Full as u32;
123const PAGE_STATE_FREEING: u32 = PageState::Freeing as u32;
124const PAGE_STATE_CORRUPT: u32 = PageState::Corrupt as u32;
125const PAGE_STATE_INVALID: u32 = PageState::Invalid as u32;
126
127impl From<u32> for PageState {
128    fn from(val: u32) -> Self {
129        match val {
130            PAGE_STATE_UNINITIALIZED => PageState::Uninitialized,
131            PAGE_STATE_ACTIVE => PageState::Active,
132            PAGE_STATE_FULL => PageState::Full,
133            PAGE_STATE_FREEING => PageState::Freeing,
134            PAGE_STATE_CORRUPT => PageState::Corrupt,
135            PAGE_STATE_INVALID => PageState::Invalid,
136            _ => PageState::Corrupt,
137        }
138    }
139}
140
141#[derive(strum::FromRepr, strum::Display, Debug, Eq, PartialEq, Copy, Clone)]
142#[repr(u8)]
143#[cfg_attr(feature = "defmt", derive(defmt::Format))]
144pub enum ItemType {
145    U8 = 0x01,
146    I8 = 0x11,
147    U16 = 0x02,
148    I16 = 0x12,
149    U32 = 0x04,
150    I32 = 0x14,
151    U64 = 0x08,
152    I64 = 0x18,
153    Sized = 0x21,
154    Blob = 0x41,
155    BlobData = 0x42,
156    BlobIndex = 0x48,
157    Any = 0xff,
158}
159
160impl ItemType {
161    pub(crate) fn get_primitive_bytes_width(&self) -> Result<usize, Error> {
162        match self {
163            ItemType::U8 | ItemType::I8 => Ok(1),
164            ItemType::U16 | ItemType::I16 => Ok(2),
165            ItemType::U32 | ItemType::I32 => Ok(4),
166            ItemType::U64 | ItemType::I64 => Ok(8),
167            _ => Err(Error::ItemTypeMismatch(*self)),
168        }
169    }
170}
171
172#[repr(C, packed)]
173#[derive(Copy, Clone)]
174pub(crate) struct PageHeader {
175    pub(crate) state: u32,
176    pub(crate) sequence: u32,
177    pub(crate) version: u8,
178    pub(crate) _unused: [u8; 19],
179    pub(crate) crc: u32,
180}
181
182pub(crate) union PageHeaderRaw {
183    pub(crate) page_header: PageHeader,
184    pub(crate) raw: [u8; size_of::<PageHeader>()],
185}
186
187impl From<PageHeader> for ThinPageHeader {
188    fn from(val: PageHeader) -> Self {
189        ThinPageHeader {
190            state: PageState::from(val.state).into(),
191            sequence: val.sequence,
192            version: val.version,
193            crc: val.crc,
194        }
195    }
196}
197
198impl Debug for PageHeader {
199    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
200        let state = self.state;
201        let sequence = self.sequence;
202        let version = self.version;
203        let crc = self.crc;
204        match state {
205            PAGE_STATE_FULL | PAGE_STATE_ACTIVE => {
206                f.write_fmt(format_args!("PageHeader {{ state: {state:?>13}, sequence: {sequence:>4}, version: 0x{version:0>2x}, crc: 0x{crc:0>4x}}}"))
207            }
208            _ => f.write_fmt(format_args!("PageHeader {{ state: {state:?>13} }}"))
209        }
210    }
211}
212
213impl PageHeader {
214    pub(crate) fn calculate_crc32(&self, crc32: FnCrc32) -> u32 {
215        let buf: [u8; 32] = unsafe { transmute(*self) };
216        let interesting_parts = &buf[4..28];
217        crc32(u32::MAX, interesting_parts)
218    }
219}
220
221#[repr(C, packed)]
222#[derive(Copy, Clone, PartialEq)]
223pub(crate) struct Item {
224    pub(crate) namespace_index: u8,
225    pub(crate) type_: ItemType, // name stolen from the Linux kernel
226    pub(crate) span: u8,
227    pub(crate) chunk_index: u8,
228    pub(crate) crc: u32,
229    pub(crate) key: Key,
230    pub(crate) data: ItemData,
231}
232pub(crate) union RawItem {
233    pub(crate) item: Item,
234    pub(crate) raw: [u8; size_of::<Item>()],
235}
236
237impl Item {
238    #[cfg(feature = "debug-logs")]
239    fn get_primitive(&self) -> Result<u64, Error> {
240        let width = match self.type_ {
241            ItemType::I8 | ItemType::I16 | ItemType::I32 | ItemType::I64 => (self.type_ as u8) - 0x10,
242            ItemType::U8 | ItemType::U16 | ItemType::U32 | ItemType::U64 => self.type_ as u8,
243            _ => return Err(Error::ItemTypeMismatch(self.type_)),
244        };
245
246        let mut mask = 0u64;
247        for i in 0..width {
248            mask |= u64::from(u8::MAX) << (i * 8)
249        }
250        Ok(unsafe { self.data.primitive & mask })
251    }
252}
253
254#[repr(C, packed)]
255#[derive(Copy, Clone)]
256pub(crate) union ItemData {
257    pub(crate) raw: [u8; 8],
258    pub(crate) primitive: u64,
259    pub(crate) sized: ItemDataSized,
260    pub(crate) blob_index: ItemDataBlobIndex,
261}
262
263impl PartialEq for ItemData {
264    fn eq(&self, other: &Self) -> bool {
265        unsafe { self.raw == other.raw }
266    }
267}
268
269#[repr(C, packed)]
270#[derive(Copy, Clone)]
271pub(crate) struct ItemDataSized {
272    pub(crate) size: u16,
273    _reserved: u16,
274    pub(crate) crc: u32,
275}
276
277impl ItemDataSized {
278    pub(crate) fn new(size: u16, crc: u32) -> Self {
279        Self {
280            size,
281            _reserved: u16::MAX,
282            crc,
283        }
284    }
285}
286
287#[cfg(feature = "debug-logs")]
288impl Debug for ItemDataSized {
289    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
290        let size = self.size;
291        let crc = self.crc;
292        f.write_fmt(format_args!("ItemDataSized {{size: {size}, crc: {crc:0>8x}}}"))
293    }
294}
295
296#[repr(C, packed)]
297#[derive(Copy, Clone)]
298pub(crate) struct ItemDataBlobIndex {
299    pub(crate) size: u32,
300    pub(crate) chunk_count: u8,
301    pub(crate) chunk_start: u8,
302}
303
304#[cfg(feature = "debug-logs")]
305impl Debug for ItemDataBlobIndex {
306    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
307        let size = self.size;
308        let chunk_count = self.chunk_count;
309        let chunk_start = self.chunk_start;
310        f.write_fmt(format_args!(
311            "ItemDataBlobIndex {{size: {size}, chunk_count: {chunk_count}, chunk_start: {chunk_start}}}"
312        ))
313    }
314}
315
316impl Item {
317    pub(crate) fn calculate_hash(&self, crc32: FnCrc32) -> u24 {
318        Self::calculate_hash_ref(crc32, self.namespace_index, &self.key, self.chunk_index)
319    }
320
321    /// `calculate_hash_ref` follows the details of the C++ implementation and accepts more
322    /// collisions in favor of memory efficiency
323    pub(crate) fn calculate_hash_ref(crc32: FnCrc32, namespace_index: u8, key: &Key, chunk_index: u8) -> u24 {
324        let mut result = u32::MAX;
325        result = crc32(result, &[namespace_index]);
326        result = crc32(result, unsafe { from_raw_parts(key.0.as_ptr(), key.0.len()) });
327        result = crc32(result, &[chunk_index]);
328        u24::from_u32(result & 0xFFFFFF)
329    }
330
331    pub(crate) fn calculate_crc32(&self, crc32: FnCrc32) -> u32 {
332        let buf: [u8; 32] = unsafe { transmute(*self) };
333        let mut result = u32::MAX;
334        result = crc32(result, &buf[0..4]);
335        result = crc32(result, self.key.0.as_ref());
336        result = unsafe { crc32(result, &self.data.raw) };
337        result
338    }
339}
340
341#[cfg(feature = "debug-logs")]
342impl Debug for Item {
343    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
344        let ns_idx = self.namespace_index;
345        let type_ = self.type_;
346        let span = self.span;
347        let chunk_index = self.chunk_index;
348        let crc = self.crc;
349        let key = slice_with_nullbytes_to_str(&self.key.0);
350        let value = match type_ {
351            ItemType::Sized | ItemType::BlobData => unsafe { format!("{:?}", self.data.sized) },
352            ItemType::Blob | ItemType::BlobIndex => unsafe { format!("{:?}", self.data.blob_index) },
353            ItemType::I8
354            | ItemType::I16
355            | ItemType::I32
356            | ItemType::I64
357            | ItemType::U8
358            | ItemType::U16
359            | ItemType::U32
360            | ItemType::U64 => format!("{}", self.get_primitive().unwrap()),
361            ItemType::Any => unsafe { format!("{:?}", self.data.raw) },
362        };
363        f.write_fmt(format_args!("Item {{ns_idx: {ns_idx}, type: {type_:<9?}, span: {span}, chunk_idx: {chunk_index:>3}, crc: {crc:0>8x}, key: '{key}', value: {value}}}"))
364    }
365}
366
367/// We know that keys and namespace names are saved in 16 byte arrays. Because they are originally C
368/// strings they are followed by a null terminator in case they are shorter than 16 byte. We have to
369/// slice before the null terminator if we want to transmute them to a str.
370#[cfg(feature = "debug-logs")]
371pub(crate) fn slice_with_nullbytes_to_str(raw: &[u8]) -> &str {
372    let sliced = match raw.iter().position(|&e| e == 0x00) {
373        None => raw,
374        Some(idx) => &raw[..idx],
375    };
376    unsafe { core::str::from_utf8_unchecked(sliced) }
377}
378
379#[inline(always)]
380pub(crate) fn write_aligned<T: Platform>(hal: &mut T, offset: u32, bytes: &[u8]) -> Result<(), T::Error> {
381    #[cfg(feature = "defmt")]
382    trace!("write_aligned @{:#08x}: [{}]", offset, bytes.len());
383
384    if bytes.len().is_multiple_of(T::WRITE_SIZE) {
385        hal.write(offset, bytes)
386    } else {
387        let pivot = T::align_write_floor(bytes.len());
388        let header = &bytes[..pivot];
389        let trailer = &bytes[pivot..];
390        if !header.is_empty() {
391            hal.write(offset, header)?;
392        }
393
394        // no need to write the trailer if remaining data is all ones - this the default state of
395        // the flash
396        if bytes[pivot..].iter().any(|&e| e != 0xFF) {
397            let mut buf = vec![0xFFu8; T::WRITE_SIZE];
398            buf[..trailer.len()].copy_from_slice(trailer);
399            hal.write(offset + (pivot as u32), &buf)?
400        }
401
402        Ok(())
403    }
404}