1#![no_std]
28
29#[derive(Debug, Copy, Clone, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Error {
33 InvalidMagicHeader,
35 WrongSize,
37 UnknownVersion,
39 BufferTooSmall,
41 FilenameTooLong,
43 NonUnicodeFilename,
45 SinkError,
47}
48
49#[derive(Debug, Copy, Clone, PartialEq, Eq)]
51pub enum FormatVersion {
52 Version100 = 1,
54}
55
56pub struct RomFs<'a> {
58 contents: &'a [u8],
59}
60
61impl<'a> RomFs<'a> {
62 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 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 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 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 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
169pub 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 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 self.contents = &[];
197 Some(Err(e))
198 }
199 }
200 }
201}
202
203#[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 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 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#[derive(Debug, PartialEq, Eq)]
268pub struct Entry<S, T>
269where
270 S: AsRef<str>,
271 T: AsRef<[u8]>,
272{
273 pub metadata: EntryMetadata<S>,
275 pub contents: T,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct EntryMetadata<S>
287where
288 S: AsRef<str>,
289{
290 pub file_name: S,
295 pub ctime: neotron_api::file::Time,
297 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 pub const SIZE: usize = Self::TIMESTAMP_OFFSET + Self::TIMESTAMP_SIZE;
314
315 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 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 fn write_into<SINK>(&self, sink: &mut SINK) -> Result<usize, Error>
372 where
373 SINK: embedded_io::Write,
374 {
375 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 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 let file_size = self.file_size.to_be_bytes();
389 sink.write_all(&file_size).map_err(|_| Error::SinkError)?;
390 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 0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
418 0x00, 0x01, 0x00, 0x00,
420 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 0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
434 0x00, 0x01, 0x00, 0x00,
436 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 0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x54,
448 0x00, 0x01, 0x00, 0x00,
450 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 0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
462 0x00, 0x01, 0x00, 0x01,
464 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 0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
476 0x00, 0x01, 0x00, 0x00,
478 0x00, 0x00, 0x00, 0x2C,
480 0x52, 0x45, 0x41, 0x44, 0x4d, 0x45, 0x2e, 0x54, 0x58, 0x54, 0x00, 0x00, 0x00, 0x00,
482 0x00, 0x00, 0x00, 0x04,
484 0x35, 0x0A, 0x0B, 0x14, 0x05, 0x10,
486 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 0x4e, 0x65, 0x6f, 0x52, 0x4f, 0x4d, 0x46, 0x53,
515 0x00, 0x01, 0x00, 0x00,
517 0x00, 0x00, 0x00, 0x47,
519 b'R', b'E', b'A', b'D', b'M', b'E', b'.', b'T', b'X', b'T', 0x00, 0x00, 0x00, 0x00,
521 0x00, 0x00, 0x00, 0x04,
523 0x35, 0x0A, 0x0B, 0x14, 0x05, 0x10,
525 0x12, 0x34, 0x56, 0x78,
527 b'H', b'E', b'L', b'L', b'O', b'.', b'D', b'O', b'C', 0x00, 0x00, 0x00, 0x00, 0x00,
529 0x00, 0x00, 0x00, 0x03,
531 0x35, 0x0A, 0x0B, 0x14, 0x05, 0x11,
533 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