1use embedded_storage::Region;
11
12pub const PARTITION_TABLE_MAX_LEN: usize = 0xC00;
14
15const PARTITION_TABLE_OFFSET: u32 =
16 esp_config::esp_config_int!(u32, "ESP_BOOTLOADER_ESP_IDF_CONFIG_PARTITION_TABLE_OFFSET");
17
18const RAW_ENTRY_LEN: usize = 32;
19const ENTRY_MAGIC: u16 = 0x50aa;
20const MD5_MAGIC: u16 = 0xebeb;
21
22pub struct PartitionEntry<'a> {
24 pub(crate) binary: &'a [u8; RAW_ENTRY_LEN],
25}
26
27impl<'a> PartitionEntry<'a> {
28 fn new(binary: &'a [u8; RAW_ENTRY_LEN]) -> Self {
29 Self { binary }
30 }
31
32 pub fn magic(&self) -> u16 {
34 u16::from_le_bytes(unwrap!(self.binary[..2].try_into()))
35 }
36
37 pub fn raw_type(&self) -> u8 {
39 self.binary[2]
40 }
41
42 pub fn raw_subtype(&self) -> u8 {
44 self.binary[3]
45 }
46
47 pub fn offset(&self) -> u32 {
49 u32::from_le_bytes(unwrap!(self.binary[4..][..4].try_into()))
50 }
51
52 pub fn len(&self) -> u32 {
54 u32::from_le_bytes(unwrap!(self.binary[8..][..4].try_into()))
55 }
56
57 pub fn is_empty(&self) -> bool {
59 self.len() == 0
60 }
61
62 pub fn label(&self) -> &'a [u8] {
64 &self.binary[12..][..16]
65 }
66
67 pub fn label_as_str(&self) -> &'a str {
69 let array = self.label();
70 let len = array
71 .iter()
72 .position(|b| *b == 0 || *b == 0xff)
73 .unwrap_or(array.len());
74 unsafe {
75 core::str::from_utf8_unchecked(core::slice::from_raw_parts(array.as_ptr().cast(), len))
76 }
77 }
78
79 pub fn flags(&self) -> u32 {
82 u32::from_le_bytes(unwrap!(self.binary[28..][..4].try_into()))
83 }
84
85 pub fn is_read_only(&self) -> bool {
87 self.flags() & 0b01 != 0
88 }
89
90 pub fn is_encrypted(&self) -> bool {
92 self.flags() & 0b10 != 0
93 }
94
95 pub fn partition_type(&self) -> PartitionType {
97 match self.raw_type() {
98 0 => PartitionType::App(unwrap!(self.raw_subtype().try_into())),
99 1 => PartitionType::Data(unwrap!(self.raw_subtype().try_into())),
100 2 => PartitionType::Bootloader(unwrap!(self.raw_subtype().try_into())),
101 3 => PartitionType::PartitionTable(unwrap!(self.raw_subtype().try_into())),
102 _ => unreachable!(),
103 }
104 }
105
106 pub fn as_embedded_storage<F>(&'a self, flash: &'a mut F) -> FlashRegion<'a, F>
110 where
111 F: embedded_storage::ReadStorage,
112 {
113 FlashRegion { raw: self, flash }
114 }
115}
116
117impl core::fmt::Debug for PartitionEntry<'_> {
118 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119 f.debug_struct("PartitionEntry")
120 .field("magic", &self.magic())
121 .field("raw_type", &self.raw_type())
122 .field("raw_subtype", &self.raw_subtype())
123 .field("offset", &self.offset())
124 .field("len", &self.len())
125 .field("label", &self.label_as_str())
126 .field("flags", &self.flags())
127 .field("is_read_only", &self.is_read_only())
128 .field("is_encrypted", &self.is_encrypted())
129 .finish()
130 }
131}
132
133#[cfg(feature = "defmt")]
134impl defmt::Format for PartitionEntry<'_> {
135 fn format(&self, fmt: defmt::Formatter) {
136 defmt::write!(
137 fmt,
138 "PartitionEntry (\
139 magic = {}, \
140 raw_type = {}, \
141 raw_subtype = {}, \
142 offset = {}, \
143 len = {}, \
144 label = {}, \
145 flags = {}, \
146 is_read_only = {}, \
147 is_encrypted = {}\
148 )",
149 self.magic(),
150 self.raw_type(),
151 self.raw_subtype(),
152 self.offset(),
153 self.len(),
154 self.label_as_str(),
155 self.flags(),
156 self.is_read_only(),
157 self.is_encrypted()
158 )
159 }
160}
161
162#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, strum::Display)]
164#[cfg_attr(feature = "defmt", derive(defmt::Format))]
165pub enum Error {
166 Invalid,
168 OutOfBounds,
170 StorageError,
172 WriteProtected,
174 InvalidPartition {
176 expected_size: usize,
177 expected_type: PartitionType,
178 },
179 InvalidState,
181}
182
183impl core::error::Error for Error {}
184
185pub struct PartitionTable<'a> {
187 binary: &'a [[u8; RAW_ENTRY_LEN]],
188 entries: usize,
189}
190
191impl<'a> PartitionTable<'a> {
192 fn new(binary: &'a [u8]) -> Result<Self, Error> {
193 if binary.len() > PARTITION_TABLE_MAX_LEN {
194 return Err(Error::Invalid);
195 }
196
197 if binary.len() % RAW_ENTRY_LEN != 0 {
198 return Err(Error::Invalid);
199 }
200
201 if binary.is_empty() {
202 return Ok(Self {
203 binary: &[],
204 entries: 0,
205 });
206 }
207
208 let binary = unsafe {
210 core::slice::from_raw_parts(
211 binary.as_ptr() as *const [u8; RAW_ENTRY_LEN],
212 binary.len() / RAW_ENTRY_LEN,
213 )
214 };
215
216 let mut raw = Self {
217 binary,
218 entries: binary.len(),
219 };
220
221 #[cfg(feature = "validation")]
222 {
223 let (hash, index) = {
224 let mut i = 0;
225 loop {
226 if let Ok(entry) = raw.get_partition(i) {
227 if entry.magic() == MD5_MAGIC {
228 break (&entry.binary[16..][..16], i);
229 }
230
231 i += 1;
232 if i >= raw.entries {
233 return Err(Error::Invalid);
234 }
235 }
236 }
237 };
238
239 use md5::Digest;
240 let mut hasher = md5::Md5::new();
241
242 for i in 0..index {
243 hasher.update(raw.binary[i]);
244 }
245 let calculated_hash = hasher.finalize();
246
247 if *calculated_hash != *hash {
248 return Err(Error::Invalid);
249 }
250 }
251
252 let entries = {
253 let mut i = 0;
254 loop {
255 if let Ok(entry) = raw.get_partition(i) {
256 if entry.magic() != ENTRY_MAGIC {
257 break;
258 }
259
260 i += 1;
261
262 if i == raw.entries {
263 break;
264 }
265 } else {
266 return Err(Error::Invalid);
267 }
268 }
269 i
270 };
271
272 raw.entries = entries;
273
274 Ok(raw)
275 }
276
277 pub fn len(&self) -> usize {
279 self.entries
280 }
281
282 pub fn is_empty(&self) -> bool {
284 self.entries == 0
285 }
286
287 pub fn get_partition(&self, index: usize) -> Result<PartitionEntry<'a>, Error> {
289 if index >= self.entries {
290 return Err(Error::OutOfBounds);
291 }
292 Ok(PartitionEntry::new(&self.binary[index]))
293 }
294
295 pub fn find_partition(&self, pt: PartitionType) -> Result<Option<PartitionEntry<'a>>, Error> {
297 for i in 0..self.entries {
298 let entry = self.get_partition(i)?;
299 if entry.partition_type() == pt {
300 return Ok(Some(entry));
301 }
302 }
303 Ok(None)
304 }
305}
306
307#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
309#[cfg_attr(feature = "defmt", derive(defmt::Format))]
310pub enum PartitionType {
311 App(AppPartitionSubType),
313 Data(DataPartitionSubType),
315 Bootloader(BootloaderPartitionSubType),
317 PartitionTable(PartitionTablePartitionSubType),
319}
320
321#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
323#[cfg_attr(feature = "defmt", derive(defmt::Format))]
324#[repr(u8)]
325pub enum RawPartitionType {
326 App = 0,
328 Data,
330 Bootloader,
332 PartitionTable,
334}
335
336#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, strum::FromRepr)]
338#[cfg_attr(feature = "defmt", derive(defmt::Format))]
339#[repr(u8)]
340pub enum AppPartitionSubType {
341 Factory = 0,
343 Ota0 = 0x10,
345 Ota1,
347 Ota2,
349 Ota3,
351 Ota4,
353 Ota5,
355 Ota6,
357 Ota7,
359 Ota8,
361 Ota9,
363 Ota10,
365 Ota11,
367 Ota12,
369 Ota13,
371 Ota14,
373 Ota15,
375 Test,
377}
378
379impl TryFrom<u8> for AppPartitionSubType {
380 type Error = Error;
381
382 fn try_from(value: u8) -> Result<Self, Self::Error> {
383 AppPartitionSubType::from_repr(value).ok_or(Error::Invalid)
384 }
385}
386
387#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, strum::FromRepr)]
389#[cfg_attr(feature = "defmt", derive(defmt::Format))]
390#[repr(u8)]
391pub enum DataPartitionSubType {
392 Ota = 0,
396 Phy,
399 Nvs,
401 Coredump,
403 NvsKeys,
405 EfuseEm,
407 Undefined,
410 Fat = 0x81,
412 Spiffs = 0x82,
414 LittleFs = 0x83,
416}
417
418impl TryFrom<u8> for DataPartitionSubType {
419 type Error = Error;
420
421 fn try_from(value: u8) -> Result<Self, Self::Error> {
422 DataPartitionSubType::from_repr(value).ok_or(Error::Invalid)
423 }
424}
425
426#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, strum::FromRepr)]
428#[cfg_attr(feature = "defmt", derive(defmt::Format))]
429#[repr(u8)]
430pub enum BootloaderPartitionSubType {
431 Primary = 0,
433 Ota = 1,
436}
437
438impl TryFrom<u8> for BootloaderPartitionSubType {
439 type Error = Error;
440
441 fn try_from(value: u8) -> Result<Self, Self::Error> {
442 BootloaderPartitionSubType::from_repr(value).ok_or(Error::Invalid)
443 }
444}
445
446#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, strum::FromRepr)]
448#[cfg_attr(feature = "defmt", derive(defmt::Format))]
449#[repr(u8)]
450pub enum PartitionTablePartitionSubType {
451 Primary = 0,
453 Ota = 1,
456}
457
458impl TryFrom<u8> for PartitionTablePartitionSubType {
459 type Error = Error;
460
461 fn try_from(value: u8) -> Result<Self, Self::Error> {
462 PartitionTablePartitionSubType::from_repr(value).ok_or(Error::Invalid)
463 }
464}
465
466pub fn read_partition_table<'a>(
471 flash: &mut impl embedded_storage::Storage,
472 storage: &'a mut [u8],
473) -> Result<PartitionTable<'a>, Error> {
474 flash
475 .read(PARTITION_TABLE_OFFSET, storage)
476 .map_err(|_e| Error::StorageError)?;
477
478 PartitionTable::new(storage)
479}
480
481#[derive(Debug)]
486#[cfg_attr(feature = "defmt", derive(defmt::Format))]
487pub struct FlashRegion<'a, F> {
488 pub(crate) raw: &'a PartitionEntry<'a>,
489 pub(crate) flash: &'a mut F,
490}
491
492impl<F> embedded_storage::Region for FlashRegion<'_, F> {
493 fn contains(&self, address: u32) -> bool {
494 address >= self.raw.offset() && address < self.raw.offset() + self.raw.len()
495 }
496}
497
498impl<F> embedded_storage::ReadStorage for FlashRegion<'_, F>
499where
500 F: embedded_storage::ReadStorage,
501{
502 type Error = Error;
503
504 fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
505 let address = offset + self.raw.offset();
506
507 if !self.contains(address) {
508 return Err(Error::OutOfBounds);
509 }
510
511 if !self.contains(address + bytes.len() as u32) {
512 return Err(Error::OutOfBounds);
513 }
514
515 self.flash
516 .read(address, bytes)
517 .map_err(|_e| Error::StorageError)
518 }
519
520 fn capacity(&self) -> usize {
521 self.raw.len() as _
522 }
523}
524
525impl<F> embedded_storage::Storage for FlashRegion<'_, F>
526where
527 F: embedded_storage::Storage,
528{
529 fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
530 let address = offset + self.raw.offset();
531
532 if self.raw.is_read_only() {
533 return Err(Error::WriteProtected);
534 }
535
536 if !self.contains(address) {
537 return Err(Error::OutOfBounds);
538 }
539
540 if !self.contains(address + bytes.len() as u32) {
541 return Err(Error::OutOfBounds);
542 }
543
544 self.flash
545 .write(address, bytes)
546 .map_err(|_e| Error::StorageError)
547 }
548}
549
550impl embedded_storage::nor_flash::NorFlashError for Error {
551 fn kind(&self) -> embedded_storage::nor_flash::NorFlashErrorKind {
552 match self {
553 Error::OutOfBounds => embedded_storage::nor_flash::NorFlashErrorKind::OutOfBounds,
554 _ => embedded_storage::nor_flash::NorFlashErrorKind::Other,
555 }
556 }
557}
558
559impl<F> embedded_storage::nor_flash::ErrorType for FlashRegion<'_, F> {
560 type Error = Error;
561}
562
563impl<F> embedded_storage::nor_flash::ReadNorFlash for FlashRegion<'_, F>
564where
565 F: embedded_storage::nor_flash::ReadNorFlash,
566{
567 const READ_SIZE: usize = F::READ_SIZE;
568
569 fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
570 let address = offset + self.raw.offset();
571
572 if !self.contains(address) {
573 return Err(Error::OutOfBounds);
574 }
575
576 if !self.contains(address + bytes.len() as u32) {
577 return Err(Error::OutOfBounds);
578 }
579
580 self.flash
581 .read(address, bytes)
582 .map_err(|_e| Error::StorageError)
583 }
584
585 fn capacity(&self) -> usize {
586 self.flash.capacity()
587 }
588}
589
590impl<F> embedded_storage::nor_flash::NorFlash for FlashRegion<'_, F>
591where
592 F: embedded_storage::nor_flash::NorFlash,
593{
594 const WRITE_SIZE: usize = F::WRITE_SIZE;
595
596 const ERASE_SIZE: usize = F::ERASE_SIZE;
597
598 fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
599 let address_from = from + self.raw.offset();
600 let address_to = to + self.raw.offset();
601
602 if self.raw.is_read_only() {
603 return Err(Error::WriteProtected);
604 }
605
606 if !self.contains(address_from) {
607 return Err(Error::OutOfBounds);
608 }
609
610 if !self.contains(address_to) {
611 return Err(Error::OutOfBounds);
612 }
613
614 self.flash
615 .erase(address_from, address_to)
616 .map_err(|_e| Error::StorageError)
617 }
618
619 fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
620 let address = offset + self.raw.offset();
621
622 if self.raw.is_read_only() {
623 return Err(Error::WriteProtected);
624 }
625
626 if !self.contains(address) {
627 return Err(Error::OutOfBounds);
628 }
629
630 if !self.contains(address + bytes.len() as u32) {
631 return Err(Error::OutOfBounds);
632 }
633
634 self.flash
635 .write(address, bytes)
636 .map_err(|_e| Error::StorageError)
637 }
638}
639
640impl<F> embedded_storage::nor_flash::MultiwriteNorFlash for FlashRegion<'_, F> where
641 F: embedded_storage::nor_flash::MultiwriteNorFlash
642{
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648
649 static SIMPLE: &[u8] = include_bytes!("../testdata/single_factory_no_ota.bin");
650 static OTA: &[u8] = include_bytes!("../testdata/factory_app_two_ota.bin");
651
652 #[test]
653 fn read_simple() {
654 let pt = PartitionTable::new(SIMPLE).unwrap();
655
656 assert_eq!(3, pt.len());
657
658 assert_eq!(1, pt.get_partition(0).unwrap().raw_type());
659 assert_eq!(1, pt.get_partition(1).unwrap().raw_type());
660 assert_eq!(0, pt.get_partition(2).unwrap().raw_type());
661
662 assert_eq!(2, pt.get_partition(0).unwrap().raw_subtype());
663 assert_eq!(1, pt.get_partition(1).unwrap().raw_subtype());
664 assert_eq!(0, pt.get_partition(2).unwrap().raw_subtype());
665
666 assert_eq!(
667 PartitionType::Data(DataPartitionSubType::Nvs),
668 pt.get_partition(0).unwrap().partition_type()
669 );
670 assert_eq!(
671 PartitionType::Data(DataPartitionSubType::Phy),
672 pt.get_partition(1).unwrap().partition_type()
673 );
674 assert_eq!(
675 PartitionType::App(AppPartitionSubType::Factory),
676 pt.get_partition(2).unwrap().partition_type()
677 );
678
679 assert_eq!(0x9000, pt.get_partition(0).unwrap().offset());
680 assert_eq!(0xf000, pt.get_partition(1).unwrap().offset());
681 assert_eq!(0x10000, pt.get_partition(2).unwrap().offset());
682
683 assert_eq!(0x6000, pt.get_partition(0).unwrap().len());
684 assert_eq!(0x1000, pt.get_partition(1).unwrap().len());
685 assert_eq!(0x100000, pt.get_partition(2).unwrap().len());
686
687 assert_eq!("nvs", pt.get_partition(0).unwrap().label_as_str());
688 assert_eq!("phy_init", pt.get_partition(1).unwrap().label_as_str());
689 assert_eq!("factory", pt.get_partition(2).unwrap().label_as_str());
690
691 assert_eq!(false, pt.get_partition(0).unwrap().is_read_only());
692 assert_eq!(false, pt.get_partition(1).unwrap().is_read_only());
693 assert_eq!(false, pt.get_partition(2).unwrap().is_read_only());
694
695 assert_eq!(false, pt.get_partition(0).unwrap().is_encrypted());
696 assert_eq!(false, pt.get_partition(1).unwrap().is_encrypted());
697 assert_eq!(false, pt.get_partition(2).unwrap().is_encrypted());
698 }
699
700 #[test]
701 fn read_ota() {
702 let pt = PartitionTable::new(OTA).unwrap();
703
704 assert_eq!(6, pt.len());
705
706 assert_eq!(1, pt.get_partition(0).unwrap().raw_type());
707 assert_eq!(1, pt.get_partition(1).unwrap().raw_type());
708 assert_eq!(1, pt.get_partition(2).unwrap().raw_type());
709 assert_eq!(0, pt.get_partition(3).unwrap().raw_type());
710 assert_eq!(0, pt.get_partition(4).unwrap().raw_type());
711 assert_eq!(0, pt.get_partition(5).unwrap().raw_type());
712
713 assert_eq!(2, pt.get_partition(0).unwrap().raw_subtype());
714 assert_eq!(0, pt.get_partition(1).unwrap().raw_subtype());
715 assert_eq!(1, pt.get_partition(2).unwrap().raw_subtype());
716 assert_eq!(0, pt.get_partition(3).unwrap().raw_subtype());
717 assert_eq!(0x10, pt.get_partition(4).unwrap().raw_subtype());
718 assert_eq!(0x11, pt.get_partition(5).unwrap().raw_subtype());
719
720 assert_eq!(
721 PartitionType::Data(DataPartitionSubType::Nvs),
722 pt.get_partition(0).unwrap().partition_type()
723 );
724 assert_eq!(
725 PartitionType::Data(DataPartitionSubType::Ota),
726 pt.get_partition(1).unwrap().partition_type()
727 );
728 assert_eq!(
729 PartitionType::Data(DataPartitionSubType::Phy),
730 pt.get_partition(2).unwrap().partition_type()
731 );
732 assert_eq!(
733 PartitionType::App(AppPartitionSubType::Factory),
734 pt.get_partition(3).unwrap().partition_type()
735 );
736 assert_eq!(
737 PartitionType::App(AppPartitionSubType::Ota0),
738 pt.get_partition(4).unwrap().partition_type()
739 );
740 assert_eq!(
741 PartitionType::App(AppPartitionSubType::Ota1),
742 pt.get_partition(5).unwrap().partition_type()
743 );
744
745 assert_eq!(0x9000, pt.get_partition(0).unwrap().offset());
746 assert_eq!(0xd000, pt.get_partition(1).unwrap().offset());
747 assert_eq!(0xf000, pt.get_partition(2).unwrap().offset());
748 assert_eq!(0x10000, pt.get_partition(3).unwrap().offset());
749 assert_eq!(0x110000, pt.get_partition(4).unwrap().offset());
750 assert_eq!(0x210000, pt.get_partition(5).unwrap().offset());
751
752 assert_eq!(0x4000, pt.get_partition(0).unwrap().len());
753 assert_eq!(0x2000, pt.get_partition(1).unwrap().len());
754 assert_eq!(0x1000, pt.get_partition(2).unwrap().len());
755 assert_eq!(0x100000, pt.get_partition(3).unwrap().len());
756 assert_eq!(0x100000, pt.get_partition(4).unwrap().len());
757 assert_eq!(0x100000, pt.get_partition(5).unwrap().len());
758
759 assert_eq!("nvs", pt.get_partition(0).unwrap().label_as_str());
760 assert_eq!("otadata", pt.get_partition(1).unwrap().label_as_str());
761 assert_eq!("phy_init", pt.get_partition(2).unwrap().label_as_str());
762 assert_eq!("factory", pt.get_partition(3).unwrap().label_as_str());
763 assert_eq!("ota_0", pt.get_partition(4).unwrap().label_as_str());
764 assert_eq!("ota_1", pt.get_partition(5).unwrap().label_as_str());
765
766 assert_eq!(false, pt.get_partition(0).unwrap().is_read_only());
767 assert_eq!(false, pt.get_partition(1).unwrap().is_read_only());
768 assert_eq!(false, pt.get_partition(2).unwrap().is_read_only());
769 assert_eq!(false, pt.get_partition(3).unwrap().is_read_only());
770 assert_eq!(false, pt.get_partition(4).unwrap().is_read_only());
771 assert_eq!(false, pt.get_partition(5).unwrap().is_read_only());
772
773 assert_eq!(false, pt.get_partition(0).unwrap().is_encrypted());
774 assert_eq!(false, pt.get_partition(1).unwrap().is_encrypted());
775 assert_eq!(false, pt.get_partition(2).unwrap().is_encrypted());
776 assert_eq!(false, pt.get_partition(3).unwrap().is_encrypted());
777 assert_eq!(false, pt.get_partition(4).unwrap().is_encrypted());
778 assert_eq!(false, pt.get_partition(5).unwrap().is_encrypted());
779 }
780
781 #[test]
782 fn empty_byte_array() {
783 let pt = PartitionTable::new(&[]).unwrap();
784
785 assert_eq!(0, pt.len());
786 assert!(matches!(pt.get_partition(0), Err(Error::OutOfBounds)));
787 }
788
789 #[test]
790 fn validation_fails_wo_hash() {
791 assert!(matches!(
792 PartitionTable::new(&SIMPLE[..RAW_ENTRY_LEN * 3]),
793 Err(Error::Invalid)
794 ));
795 }
796
797 #[test]
798 fn validation_fails_wo_hash_max_entries() {
799 let mut data = [0u8; PARTITION_TABLE_MAX_LEN];
800 for i in 0..96 {
801 data[(i * RAW_ENTRY_LEN)..][..RAW_ENTRY_LEN].copy_from_slice(&SIMPLE[..32]);
802 }
803
804 assert!(matches!(PartitionTable::new(&data), Err(Error::Invalid)));
805 }
806
807 #[test]
808 fn validation_succeeds_with_enough_entries() {
809 assert_eq!(
810 3,
811 PartitionTable::new(&SIMPLE[..RAW_ENTRY_LEN * 4])
812 .unwrap()
813 .len()
814 );
815 }
816}