neotron_romfs/
lib.rs

1//! Library for creating or parsing a Neotron ROM Filing System (ROMFS) image
2//!
3//! To view the contents of a ROMFS, use a for loop:
4//!
5//! ```rust
6//! fn process_rom(data: &[u8]) -> Result<(), neotron_romfs::Error> {
7//!     let romfs = neotron_romfs::RomFs::new(data)?;
8//!     for entry in romfs {
9//!         if let Ok(entry) = entry {
10//!            println!("{} is {} bytes", entry.metadata.file_name, entry.metadata.file_size);
11//!         }
12//!     }
13//!     Ok(())
14//! }
15//! ```
16//!
17//! To open a specific file, use [`RomFs::find`]:
18//!
19//! ```rust
20//! fn process_rom(romfs: &neotron_romfs::RomFs) {
21//!     if let Some(entry) = romfs.find("HELLO.ELF") {
22//!         let data: &[u8] = entry.contents;
23//!     }
24//! }
25//! ```
26
27#![no_std]
28
29/// The ways in which this module can fail
30#[derive(Debug, Copy, Clone, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Error {
33    /// We didn't see the magic number at the start of the ROMFS
34    InvalidMagicHeader,
35    /// The given size was not the same length that the header reported
36    WrongSize,
37    /// Did not recognise the version
38    UnknownVersion,
39    /// Buffer was too small to hold ROMFS image
40    BufferTooSmall,
41    /// Filename was too long (we have a 14 byte maximum)
42    FilenameTooLong,
43    /// A filename wasn't valid UTF-8
44    NonUnicodeFilename,
45    /// There was an error writing to a sink
46    SinkError,
47}
48
49/// The different image formats we support
50#[derive(Debug, Copy, Clone, PartialEq, Eq)]
51pub enum FormatVersion {
52    /// The first version
53    Version100 = 1,
54}
55
56/// Represents a ROM Filing System (ROMFS), as backed by a byte slice in memory.
57pub struct RomFs<'a> {
58    contents: &'a [u8],
59}
60
61impl<'a> RomFs<'a> {
62    /// Mount a ROMFS using a given block of RAM
63    pub fn new(contents: &'a [u8]) -> Result<RomFs<'a>, Error> {
64        let (header, remainder) = Header::from_bytes(contents)?;
65        if contents.len() != header.total_size as usize {
66            return Err(Error::WrongSize);
67        }
68        Ok(RomFs {
69            contents: remainder,
70        })
71    }
72
73    /// Find a file in the ROMFS, by name.
74    pub fn find(&self, file_name: &str) -> Option<Entry<&str, &[u8]>> {
75        self.into_iter()
76            .flatten()
77            .find(|e| e.metadata.file_name == file_name)
78    }
79
80    /// Construct a ROMFS into the given buffer.
81    ///
82    /// Tells you how many bytes it used of the given buffer.
83    ///
84    /// The buffer must be large enough otherwise an error is returned - see
85    /// [`Self::size_required`] to calculate the size of buffer required.
86    pub fn construct<S, T>(mut buffer: &mut [u8], entries: &[Entry<S, T>]) -> Result<usize, Error>
87    where
88        S: AsRef<str>,
89        T: AsRef<[u8]>,
90    {
91        let total_size = Self::size_required(entries);
92        if buffer.len() < total_size {
93            return Err(Error::BufferTooSmall);
94        }
95        let used = Self::construct_into(&mut buffer, entries)?;
96        Ok(used)
97    }
98
99    /// Construct a ROMFS into the given embedded-io byte sink.
100    ///
101    /// Tells you how many bytes it wrote to the given buffer.
102    pub fn construct_into<S, T, SINK>(
103        buffer: &mut SINK,
104        entries: &[Entry<S, T>],
105    ) -> Result<usize, Error>
106    where
107        S: AsRef<str>,
108        T: AsRef<[u8]>,
109        SINK: embedded_io::Write,
110    {
111        let total_size = Self::size_required(entries);
112        let file_header = Header {
113            format_version: FormatVersion::Version100,
114            total_size: total_size as u32,
115        };
116        let mut used = file_header.write_into(buffer)?;
117        for entry in entries.iter() {
118            used += entry.metadata.write_into(buffer)?;
119            let contents: &[u8] = entry.contents.as_ref();
120            buffer.write_all(contents).map_err(|_| Error::SinkError)?;
121            used += contents.len();
122        }
123
124        assert_eq!(used, total_size);
125
126        Ok(total_size)
127    }
128
129    /// Tells you how many bytes you need to make a ROMFS from these entries.
130    pub fn size_required<S, T>(entries: &[Entry<S, T>]) -> usize
131    where
132        S: AsRef<str>,
133        T: AsRef<[u8]>,
134    {
135        let mut total_size: usize = Header::FIXED_SIZE;
136        for entry in entries.iter() {
137            total_size += EntryMetadata::<S>::SIZE;
138            let contents: &[u8] = entry.contents.as_ref();
139            total_size += contents.len();
140        }
141        total_size
142    }
143}
144
145impl<'a> IntoIterator for RomFs<'a> {
146    type Item = Result<Entry<&'a str, &'a [u8]>, Error>;
147
148    type IntoIter = RomFsEntryIter<'a>;
149
150    fn into_iter(self) -> Self::IntoIter {
151        RomFsEntryIter {
152            contents: self.contents,
153        }
154    }
155}
156
157impl<'a> IntoIterator for &'a RomFs<'a> {
158    type Item = Result<Entry<&'a str, &'a [u8]>, Error>;
159
160    type IntoIter = RomFsEntryIter<'a>;
161
162    fn into_iter(self) -> Self::IntoIter {
163        RomFsEntryIter {
164            contents: self.contents,
165        }
166    }
167}
168
169/// An iterator for working through the entries in a ROMFS
170pub struct RomFsEntryIter<'a> {
171    contents: &'a [u8],
172}
173
174impl<'a> Iterator for RomFsEntryIter<'a> {
175    type Item = Result<Entry<&'a str, &'a [u8]>, Error>;
176
177    fn next(&mut self) -> Option<Self::Item> {
178        if self.contents.is_empty() {
179            return None;
180        }
181        match EntryMetadata::<&str>::from_bytes(self.contents) {
182            Ok((hdr, remainder)) => {
183                if hdr.file_size as usize > remainder.len() {
184                    // stop if we run out of data
185                    return None;
186                }
187                let (contents, remainder) = remainder.split_at(hdr.file_size as usize);
188                self.contents = remainder;
189                Some(Ok(Entry {
190                    metadata: hdr,
191                    contents,
192                }))
193            }
194            Err(e) => {
195                // stop the iteration
196                self.contents = &[];
197                Some(Err(e))
198            }
199        }
200    }
201}
202
203/// Found at the start of the ROMFS image
204///
205/// In flash we have 8 bytes of magic number, four bytes of version and four
206/// bytes of length.
207#[derive(Debug, Clone, PartialEq, Eq)]
208struct Header {
209    pub format_version: FormatVersion,
210    pub total_size: u32,
211}
212
213impl Header {
214    const MAGIC_VALUE: [u8; 8] = *b"NeoROMFS";
215    const FORMAT_V100: [u8; 4] = [0x00, 0x01, 0x00, 0x00];
216    const FIXED_SIZE: usize = 8 + 4 + 4;
217
218    /// Parse a header from raw bytes.
219    fn from_bytes(data: &[u8]) -> Result<(Header, &[u8]), Error> {
220        let Some(magic_value) = data.get(0..8) else {
221            return Err(Error::BufferTooSmall);
222        };
223        if magic_value != Self::MAGIC_VALUE {
224            return Err(Error::InvalidMagicHeader);
225        }
226        let Some(format_version) = data.get(8..12) else {
227            return Err(Error::BufferTooSmall);
228        };
229        if format_version == Self::FORMAT_V100 {
230            let Some(total_size) = data.get(12..16) else {
231                return Err(Error::UnknownVersion);
232            };
233            let total_size: [u8; 4] = total_size.try_into().unwrap();
234            let total_size = u32::from_be_bytes(total_size);
235            let hdr = Header {
236                format_version: FormatVersion::Version100,
237                total_size,
238            };
239            Ok((hdr, &data[16..]))
240        } else {
241            Err(Error::UnknownVersion)
242        }
243    }
244
245    /// Write the header to the given buffer
246    fn write_into<SINK>(&self, buffer: &mut SINK) -> Result<usize, Error>
247    where
248        SINK: embedded_io::Write,
249    {
250        buffer
251            .write_all(&Self::MAGIC_VALUE)
252            .map_err(|_| Error::SinkError)?;
253        buffer
254            .write_all(match self.format_version {
255                FormatVersion::Version100 => &Self::FORMAT_V100,
256            })
257            .map_err(|_| Error::SinkError)?;
258        let size_bytes = self.total_size.to_be_bytes();
259        buffer
260            .write_all(&size_bytes)
261            .map_err(|_| Error::SinkError)?;
262        Ok(Header::FIXED_SIZE)
263    }
264}
265
266/// An entry in the ROMFS, including its contents.
267#[derive(Debug, PartialEq, Eq)]
268pub struct Entry<S, T>
269where
270    S: AsRef<str>,
271    T: AsRef<[u8]>,
272{
273    /// Metadata for this entry.
274    pub metadata: EntryMetadata<S>,
275    /// The file data for this entry.
276    ///
277    /// Call `contents.as_ref()` to get the contents as a byte slice
278    /// (`&[u8]`).
279    pub contents: T,
280}
281
282/// Metadata for an entry in the ROMFS.
283///
284/// Occupies [`Self::SIZE`] bytes of ROM when encoded.
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct EntryMetadata<S>
287where
288    S: AsRef<str>,
289{
290    /// The file name for this entry.
291    ///
292    /// Call `file_name.as_ref()` to get the contents as a string slice
293    /// (`&str`).
294    pub file_name: S,
295    /// The creation time, of the file associated with this entry.
296    pub ctime: neotron_api::file::Time,
297    /// The size, in bytes, of the file associated with this entry.
298    pub file_size: u32,
299}
300
301impl<S> EntryMetadata<S>
302where
303    S: AsRef<str>,
304{
305    const FILENAME_SIZE: usize = 14;
306    const FILENAME_OFFSET: usize = 0;
307    const FILESIZE_SIZE: usize = 4;
308    const FILESIZE_OFFSET: usize = Self::FILENAME_OFFSET + Self::FILENAME_SIZE;
309    const TIMESTAMP_SIZE: usize = 6;
310    const TIMESTAMP_OFFSET: usize = Self::FILESIZE_OFFSET + Self::FILESIZE_SIZE;
311
312    /// The size of this metadata, in bytes, when encoded as bytes.
313    pub const SIZE: usize = Self::TIMESTAMP_OFFSET + Self::TIMESTAMP_SIZE;
314
315    /// Parse out some entry metadata from raw bytes.
316    ///
317    /// We assume the bytes are correctly formatted - we can't check much here
318    /// other than the filename being valid UTF-8, or that too few bytes were
319    /// given.
320    ///
321    /// Returns the entry and the remaining unused bytes, or an error.
322    fn from_bytes(data: &[u8]) -> Result<(EntryMetadata<&str>, &[u8]), Error> {
323        let Some(file_name) =
324            data.get(Self::FILENAME_OFFSET..Self::FILENAME_OFFSET + Self::FILENAME_SIZE)
325        else {
326            return Err(Error::BufferTooSmall);
327        };
328        let Ok(file_name) = core::str::from_utf8(file_name) else {
329            return Err(Error::NonUnicodeFilename);
330        };
331        let file_name = file_name.trim_end_matches('\0');
332        let ctime = neotron_api::file::Time {
333            year_since_1970: *data
334                .get(Self::TIMESTAMP_OFFSET)
335                .ok_or(Error::BufferTooSmall)?,
336            zero_indexed_month: *data
337                .get(Self::TIMESTAMP_OFFSET + 1)
338                .ok_or(Error::BufferTooSmall)?,
339            zero_indexed_day: *data
340                .get(Self::TIMESTAMP_OFFSET + 2)
341                .ok_or(Error::BufferTooSmall)?,
342            hours: *data
343                .get(Self::TIMESTAMP_OFFSET + 3)
344                .ok_or(Error::BufferTooSmall)?,
345            minutes: *data
346                .get(Self::TIMESTAMP_OFFSET + 4)
347                .ok_or(Error::BufferTooSmall)?,
348            seconds: *data
349                .get(Self::TIMESTAMP_OFFSET + 5)
350                .ok_or(Error::BufferTooSmall)?,
351        };
352        let Some(file_size) =
353            data.get(Self::FILESIZE_OFFSET..Self::FILESIZE_OFFSET + Self::FILESIZE_SIZE)
354        else {
355            return Err(Error::BufferTooSmall);
356        };
357        // We got four bytes above so this can't fail
358        let file_size: [u8; 4] = file_size.try_into().unwrap();
359        let file_size = u32::from_be_bytes(file_size);
360        let stored_entry = EntryMetadata {
361            file_name,
362            file_size,
363            ctime,
364        };
365        Ok((stored_entry, &data[Self::SIZE..]))
366    }
367
368    /// Write this entry to the sink.
369    ///
370    /// Returns the number of bytes written.
371    fn write_into<SINK>(&self, sink: &mut SINK) -> Result<usize, Error>
372    where
373        SINK: embedded_io::Write,
374    {
375        // check the file name isn't too long
376        let file_name = self.file_name.as_ref();
377        let file_name_len = file_name.len();
378        let Some(padding_length) = Self::FILENAME_SIZE.checked_sub(file_name_len) else {
379            return Err(Error::FilenameTooLong);
380        };
381        // copy file name with null padding
382        sink.write_all(file_name.as_bytes())
383            .map_err(|_| Error::SinkError)?;
384        for _ in 0..padding_length {
385            sink.write_all(&[0u8]).map_err(|_| Error::SinkError)?;
386        }
387        // copy file size
388        let file_size = self.file_size.to_be_bytes();
389        sink.write_all(&file_size).map_err(|_| Error::SinkError)?;
390        // copy timestamp
391        sink.write_all(&[self.ctime.year_since_1970])
392            .map_err(|_| Error::SinkError)?;
393        sink.write_all(&[self.ctime.zero_indexed_month])
394            .map_err(|_| Error::SinkError)?;
395        sink.write_all(&[self.ctime.zero_indexed_day])
396            .map_err(|_| Error::SinkError)?;
397        sink.write_all(&[self.ctime.hours])
398            .map_err(|_| Error::SinkError)?;
399        sink.write_all(&[self.ctime.minutes])
400            .map_err(|_| Error::SinkError)?;
401        sink.write_all(&[self.ctime.seconds])
402            .map_err(|_| Error::SinkError)?;
403
404        Ok(Self::SIZE)
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn decode_empty() {
414        #[rustfmt::skip]
415        let data = [
416            // Magic number
417            0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
418            // Version
419            0x00, 0x01, 0x00, 0x00,
420            // Total size
421            0x00, 0x00, 0x00, 0x10,
422        ];
423        let romfs = RomFs::new(&data).unwrap();
424        let mut i = romfs.into_iter();
425        assert!(i.next().is_none());
426    }
427
428    #[test]
429    fn decode_bad_len() {
430        #[rustfmt::skip]
431        let data = [
432            // Magic number
433            0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
434            // Version
435            0x00, 0x01, 0x00, 0x00,
436            // Total size
437            0x00, 0x00, 0x00, 0x0F,
438        ];
439        assert!(RomFs::new(&data).is_err());
440    }
441
442    #[test]
443    fn decode_bad_magic() {
444        #[rustfmt::skip]
445        let data = [
446            // Magic number
447            0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x54,
448            // Version
449            0x00, 0x01, 0x00, 0x00,
450            // Total size
451            0x00, 0x00, 0x00, 0x10,
452        ];
453        assert!(RomFs::new(&data).is_err());
454    }
455
456    #[test]
457    fn decode_bad_version() {
458        #[rustfmt::skip]
459        let data = [
460            // Magic number
461            0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
462            // Version
463            0x00, 0x01, 0x00, 0x01,
464            // Total size
465            0x00, 0x00, 0x00, 0x10,
466        ];
467        assert!(RomFs::new(&data).is_err());
468    }
469
470    #[test]
471    fn decode_one_file() {
472        #[rustfmt::skip]
473        let data = [
474            // Magic number
475            0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
476            // Version
477            0x00, 0x01, 0x00, 0x00,
478            // Total size
479            0x00, 0x00, 0x00, 0x2C,
480            // File Name
481            0x52, 0x45, 0x41, 0x44, 0x4d, 0x45, 0x2e, 0x54, 0x58, 0x54, 0x00, 0x00, 0x00, 0x00,
482            // File size
483            0x00, 0x00, 0x00, 0x04,
484            // Timestamp (2023-11-12T20:05:16)
485            0x35, 0x0A, 0x0B, 0x14, 0x05, 0x10,
486            // Contents
487            0x12, 0x34, 0x56, 0x78,
488        ];
489        let romfs = RomFs::new(&data).unwrap();
490        let mut i = romfs.into_iter();
491        let first_item = i.next().unwrap().unwrap();
492        assert_eq!(first_item.metadata.file_name, "README.TXT");
493        assert_eq!(first_item.contents.len(), 4);
494        assert_eq!(first_item.contents, &[0x12, 0x34, 0x56, 0x78]);
495        assert_eq!(
496            first_item.metadata.ctime,
497            neotron_api::file::Time {
498                year_since_1970: 53,
499                zero_indexed_month: 10,
500                zero_indexed_day: 11,
501                hours: 20,
502                minutes: 5,
503                seconds: 16
504            }
505        );
506        assert!(i.next().is_none());
507    }
508
509    #[test]
510    fn decode_two_files() {
511        #[rustfmt::skip]
512        let data = [
513            // Magic number
514            0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
515            // Version
516            0x00, 0x01, 0x00, 0x00,
517            // Total size
518            0x00, 0x00, 0x00, 0x47,
519            // File Name
520            b'R', b'E', b'A', b'D', b'M', b'E', b'.', b'T', b'X', b'T', 0x00, 0x00, 0x00, 0x00,
521            // File size
522            0x00, 0x00, 0x00, 0x04,
523            // Timestamp (2023-11-12T20:05:16)
524            0x35, 0x0A, 0x0B, 0x14, 0x05, 0x10,
525            // Contents
526            0x12, 0x34, 0x56, 0x78,
527            // File Name
528            b'H', b'E', b'L', b'L', b'O', b'.', b'D', b'O', b'C', 0x00, 0x00, 0x00, 0x00, 0x00,
529            // File size
530            0x00, 0x00, 0x00, 0x03,
531            // Timestamp (2023-11-12T20:05:17)
532            0x35, 0x0A, 0x0B, 0x14, 0x05, 0x11,
533            // Contents
534            0xAB, 0xCD, 0xEF,
535        ];
536        let romfs = RomFs::new(&data).unwrap();
537        let mut i = romfs.into_iter();
538        let first_item = i.next().unwrap().unwrap();
539        assert_eq!(first_item.metadata.file_name, "README.TXT");
540        assert_eq!(first_item.contents.len(), 4);
541        assert_eq!(first_item.contents, &[0x12, 0x34, 0x56, 0x78]);
542        assert_eq!(
543            first_item.metadata.ctime,
544            neotron_api::file::Time {
545                year_since_1970: 53,
546                zero_indexed_month: 10,
547                zero_indexed_day: 11,
548                hours: 20,
549                minutes: 5,
550                seconds: 16
551            }
552        );
553        let second_item = i.next().unwrap().unwrap();
554        assert_eq!(second_item.metadata.file_name, "HELLO.DOC");
555        assert_eq!(second_item.contents.len(), 3);
556        assert_eq!(second_item.contents, &[0xAB, 0xCD, 0xEF]);
557        assert_eq!(
558            second_item.metadata.ctime,
559            neotron_api::file::Time {
560                year_since_1970: 53,
561                zero_indexed_month: 10,
562                zero_indexed_day: 11,
563                hours: 20,
564                minutes: 5,
565                seconds: 17
566            }
567        );
568        assert!(i.next().is_none());
569    }
570}
571
572// End of file