Skip to main content

ironwork_rt/module/
container.rs

1use super::ModuleError;
2use super::codec::{Reader, Writer};
3use super::crc::{crc32, extend};
4use super::strings::StringTable;
5
6pub const MAGIC: [u8; 8] = [0x89, b'I', b'W', b'M', 0x0D, 0x0A, 0x1A, 0x0A];
7
8/// Flag bit 0 of a section entry: a reader that does not know the section skips it.
9pub const OPTIONAL: u32 = 1;
10
11/// The first id of the extension sections, which are always written optional.
12pub const EXTENSIONS: u32 = 0x8000;
13
14const HEADER: usize = 32;
15const ENTRY: usize = 28;
16const HEADER_CRC: usize = 28;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct Version {
20    pub major: u16,
21    pub minor: u16,
22}
23
24impl Version {
25    /// The version this ironwork writes.
26    pub const CURRENT: Self = Self { major: 0, minor: 6 };
27
28    /// The oldest version this ironwork reads (§8.1): before 1.0 the last minor whose change was not
29    /// additive, so each such change moves it, and from 1.0 the major's first minor.
30    pub const OLDEST_READABLE: Self = Self { major: 0, minor: 5 };
31
32    /// Whether a reader whose oldest readable version is `self` reads `found`: the same major, at
33    /// that minor or a later one (§8.1).
34    pub const fn reads(self, found: Self) -> bool {
35        found.major == self.major && found.minor >= self.minor
36    }
37}
38
39const _: () = assert!(
40    Version::OLDEST_READABLE.major == Version::CURRENT.major
41        && Version::OLDEST_READABLE.minor <= Version::CURRENT.minor
42        && (Version::CURRENT.major == 0 || Version::OLDEST_READABLE.minor == 0),
43    "the oldest readable version is a minor of the current major, no later than it, and x.0 from 1.0"
44);
45
46/// A section this version knows (load-module.md §3.4). Every one is required.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct Section {
49    pub id: u32,
50    pub name: &'static str,
51}
52
53impl Section {
54    pub const STRINGS: Self = Self { id: 1, name: "STRINGS" };
55    pub const DIRECTORY: Self = Self { id: 2, name: "DIRECTORY" };
56    pub const OPTIONS: Self = Self { id: 3, name: "OPTIONS" };
57    pub const LAYOUT: Self = Self { id: 4, name: "LAYOUT" };
58    pub const LIR: Self = Self { id: 5, name: "LIR" };
59    pub const SQL: Self = Self { id: 6, name: "SQL" };
60    pub const BMS: Self = Self { id: 7, name: "BMS" };
61    pub const DEBUG: Self = Self { id: 8, name: "DEBUG" };
62
63    pub const ALL: [Self; 8] =
64        [Self::STRINGS, Self::DIRECTORY, Self::OPTIONS, Self::LAYOUT, Self::LIR, Self::SQL, Self::BMS, Self::DEBUG];
65
66    pub fn by_id(id: u32) -> Option<Self> {
67        Self::ALL.into_iter().find(|s| s.id == id)
68    }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub struct SectionEntry {
73    pub id: u32,
74    pub flags: u32,
75    pub offset: u64,
76    pub length: u64,
77    pub crc: u32,
78}
79
80impl SectionEntry {
81    pub fn name(&self) -> Option<&'static str> {
82        Section::by_id(self.id).map(|s| s.name)
83    }
84
85    pub fn optional(&self) -> bool {
86        self.flags & OPTIONAL != 0
87    }
88}
89
90fn le<const N: usize>(bytes: &[u8], at: usize) -> Option<[u8; N]> {
91    bytes.get(at..)?.first_chunk().copied()
92}
93
94fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
95    le(bytes, at).map(u16::from_le_bytes)
96}
97
98fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
99    le(bytes, at).map(u32::from_le_bytes)
100}
101
102fn u64_at(bytes: &[u8], at: usize) -> Option<u64> {
103    le(bytes, at).map(u64::from_le_bytes)
104}
105
106/// Lays sections out as given, with their table, checksums and header.
107fn assemble(version: Version, features: u32, sections: &[(u32, u32, &[u8])]) -> Vec<u8> {
108    let table_end = HEADER + sections.len() * ENTRY;
109    let file_len = table_end + sections.iter().map(|s| s.2.len()).sum::<usize>();
110    let mut out = Vec::with_capacity(file_len);
111    out.extend_from_slice(&MAGIC);
112    out.extend_from_slice(&version.major.to_le_bytes());
113    out.extend_from_slice(&version.minor.to_le_bytes());
114    out.extend_from_slice(&features.to_le_bytes());
115    out.extend_from_slice(&(sections.len() as u32).to_le_bytes());
116    out.extend_from_slice(&(file_len as u64).to_le_bytes());
117    out.extend_from_slice(&[0; 4]);
118    let mut offset = table_end as u64;
119    for &(id, flags, body) in sections {
120        out.extend_from_slice(&id.to_le_bytes());
121        out.extend_from_slice(&flags.to_le_bytes());
122        out.extend_from_slice(&offset.to_le_bytes());
123        out.extend_from_slice(&(body.len() as u64).to_le_bytes());
124        out.extend_from_slice(&crc32(body).to_le_bytes());
125        offset += body.len() as u64;
126    }
127    let crc = extend(crc32(&out[..HEADER_CRC]), &out[HEADER..]);
128    out[HEADER_CRC..HEADER].copy_from_slice(&crc.to_le_bytes());
129    for &(_, _, body) in sections {
130        out.extend_from_slice(body);
131    }
132    out
133}
134
135/// Builds section bodies in id order; `finish` puts the string table first and adds the checksums.
136#[derive(Debug, Default)]
137pub struct ModuleWriter {
138    writer: Writer,
139    sections: Vec<(u32, u32, Vec<u8>)>,
140}
141
142impl ModuleWriter {
143    pub fn new() -> Self {
144        Self::default()
145    }
146
147    /// Panics if `section` does not follow the last one written, or is `STRINGS`, which `finish` writes.
148    pub fn section(&mut self, section: Section, build: impl FnOnce(&mut Writer)) {
149        self.push(section.id, 0, build);
150    }
151
152    /// An optional section a reader that does not know `id` skips. Panics if `id` is below `EXTENSIONS`.
153    pub fn extension(&mut self, id: u32, build: impl FnOnce(&mut Writer)) {
154        assert!(id >= EXTENSIONS, "extension section {id:#x} is below {EXTENSIONS:#x}");
155        self.push(id, OPTIONAL, build);
156    }
157
158    fn push(&mut self, id: u32, flags: u32, build: impl FnOnce(&mut Writer)) {
159        let last = self.sections.last().map_or(Section::STRINGS.id, |s| s.0);
160        assert!(id > last, "section {id:#x} written after section {last:#x}");
161        build(&mut self.writer);
162        let body = self.writer.take();
163        self.sections.push((id, flags, body));
164    }
165
166    /// Panics if a required section was not written.
167    pub fn finish(self) -> Vec<u8> {
168        self.finish_as(Version::CURRENT, 0)
169    }
170
171    fn finish_as(self, version: Version, features: u32) -> Vec<u8> {
172        for required in &Section::ALL[1..] {
173            assert!(self.sections.iter().any(|s| s.0 == required.id), "section {} was not written", required.name);
174        }
175        let strings = self.writer.strings().encode();
176        let mut sections = vec![(Section::STRINGS.id, 0, strings.as_slice())];
177        sections.extend(self.sections.iter().map(|(id, flags, body)| (*id, *flags, body.as_slice())));
178        assemble(version, features, &sections)
179    }
180}
181
182/// A module whose header and table are checked; a section's checksum is checked when it is read.
183#[derive(Clone, Debug)]
184pub struct Module<'a> {
185    bytes: &'a [u8],
186    version: Version,
187    sections: Vec<SectionEntry>,
188}
189
190impl<'a> Module<'a> {
191    /// Checks magic, version, length, header checksum, features, then the table, in that order.
192    pub fn read(bytes: &'a [u8]) -> Result<Self, ModuleError> {
193        let actual = bytes.len() as u64;
194        let shown = bytes.len().min(MAGIC.len());
195        if bytes[..shown] != MAGIC[..shown] {
196            return Err(ModuleError::NotAModule);
197        }
198        let truncated = ModuleError::Truncated { expected: HEADER as u64, actual };
199        let header = (
200            u16_at(bytes, 8),
201            u16_at(bytes, 10),
202            u32_at(bytes, 12),
203            u32_at(bytes, 16),
204            u64_at(bytes, 20),
205            u32_at(bytes, HEADER_CRC),
206        );
207        let (Some(major), Some(minor), Some(features), Some(count), Some(file_len), Some(stored)) = header else {
208            return Err(truncated);
209        };
210        let version = Version { major, minor };
211        if !Version::OLDEST_READABLE.reads(version) {
212            return Err(ModuleError::Version(version));
213        }
214        if actual < file_len {
215            return Err(ModuleError::Truncated { expected: file_len, actual });
216        }
217        if actual > file_len {
218            return Err(ModuleError::TrailingBytes { expected: file_len, actual });
219        }
220        let table_end = HEADER as u64 + u64::from(count) * ENTRY as u64;
221        let table = bytes.get(HEADER..table_end.min(actual) as usize).unwrap_or_default();
222        let computed = extend(crc32(&bytes[..HEADER_CRC]), table);
223        if computed != stored {
224            return Err(ModuleError::HeaderChecksum { computed, stored });
225        }
226        if features != 0 {
227            return Err(ModuleError::Feature(features));
228        }
229        let malformed = |offset, reason| ModuleError::Malformed { section: "section table", offset, reason };
230        if table_end > file_len {
231            return Err(malformed(
232                16,
233                format!("{count} sections need a table to byte {table_end}, past the end at {file_len}"),
234            ));
235        }
236
237        let mut sections: Vec<SectionEntry> = Vec::with_capacity(count as usize);
238        let mut next = table_end;
239        for at in (HEADER..table_end as usize).step_by(ENTRY) {
240            let fields = (
241                u32_at(bytes, at),
242                u32_at(bytes, at + 4),
243                u64_at(bytes, at + 8),
244                u64_at(bytes, at + 16),
245                u32_at(bytes, at + 24),
246            );
247            let (Some(id), Some(flags), Some(offset), Some(length), Some(crc)) = fields else {
248                return Err(truncated);
249            };
250            let entry = SectionEntry { id, flags, offset, length, crc };
251            if let Some(last) = sections.last()
252                && last.id >= id
253            {
254                return Err(malformed(at, format!("section {id:#x} follows section {:#x}", last.id)));
255            }
256            if offset != next {
257                return Err(malformed(at, format!("section {id:#x} begins at byte {offset}, not {next}")));
258            }
259            next = match offset.checked_add(length) {
260                Some(end) if end <= file_len => end,
261                _ => return Err(malformed(at, format!("section {id:#x} of {length} bytes runs past the end"))),
262            };
263            if flags & !OPTIONAL != 0 {
264                return Err(malformed(at, format!("section {id:#x} has flags {flags:#x}")));
265            }
266            match (Section::by_id(id), entry.optional()) {
267                (Some(known), true) => {
268                    return Err(malformed(at, format!("required section {} is flagged optional", known.name)));
269                }
270                (None, false) => return Err(ModuleError::UnknownSection(id)),
271                _ => {}
272            }
273            sections.push(entry);
274        }
275        if next != file_len {
276            return Err(malformed(HEADER, format!("the sections end at byte {next}, before the end at {file_len}")));
277        }
278        if let Some(missing) = Section::ALL.iter().find(|s| !sections.iter().any(|e| e.id == s.id)) {
279            return Err(ModuleError::MissingSection(missing.id));
280        }
281        Ok(Self { bytes, version, sections })
282    }
283
284    pub fn version(&self) -> Version {
285        self.version
286    }
287
288    /// The section table, in id order, with any optional section this reader does not know.
289    pub fn sections(&self) -> &[SectionEntry] {
290        &self.sections
291    }
292
293    /// A section's bytes, once its checksum matches.
294    pub fn body(&self, id: u32) -> Result<&'a [u8], ModuleError> {
295        let entry = self.sections.iter().find(|e| e.id == id).ok_or(ModuleError::MissingSection(id))?;
296        let body = usize::try_from(entry.offset)
297            .ok()
298            .zip(usize::try_from(entry.length).ok())
299            .and_then(|(start, len)| self.bytes.get(start..start.checked_add(len)?))
300            .ok_or(ModuleError::Truncated {
301                expected: entry.offset.saturating_add(entry.length),
302                actual: self.bytes.len() as u64,
303            })?;
304        let computed = crc32(body);
305        if computed != entry.crc {
306            return Err(ModuleError::SectionChecksum { id, computed, stored: entry.crc });
307        }
308        Ok(body)
309    }
310
311    pub fn strings(&self) -> Result<StringTable, ModuleError> {
312        StringTable::decode(self.body(Section::STRINGS.id)?)
313    }
314
315    pub fn reader<'r>(&self, section: Section, strings: &'r StringTable) -> Result<Reader<'r>, ModuleError>
316    where
317        'a: 'r,
318    {
319        Ok(Reader::new(section.name, self.body(section.id)?, strings))
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::module::codec::{Decode, Encode};
327
328    const TABLE_END: usize = HEADER + 8 * ENTRY;
329
330    fn names(texts: &[&str]) -> Vec<String> {
331        texts.iter().map(|s| (*s).to_owned()).collect()
332    }
333
334    /// A module whose DIRECTORY holds `ids`, and whose other sections each hold a zero count.
335    fn sample(ids: &[String]) -> Vec<u8> {
336        let mut w = ModuleWriter::new();
337        w.section(Section::DIRECTORY, |w| ids.to_vec().encode(w));
338        for section in &Section::ALL[2..] {
339            w.section(*section, |w| w.count(0));
340        }
341        w.finish()
342    }
343
344    /// Every known section, each with a zero count for its body.
345    fn plain() -> Vec<(u32, u32, &'static [u8])> {
346        Section::ALL.iter().map(|s| (s.id, 0, &[0u8][..])).collect()
347    }
348
349    fn read(bytes: &[u8]) -> Result<Vec<u32>, ModuleError> {
350        Module::read(bytes).map(|m| m.sections().iter().map(|e| e.id).collect())
351    }
352
353    fn reseal(bytes: &mut [u8]) {
354        let count = u32::from_le_bytes(bytes[16..20].try_into().unwrap()) as usize;
355        let end = (HEADER + count * ENTRY).min(bytes.len());
356        let crc = extend(crc32(&bytes[..HEADER_CRC]), &bytes[HEADER..end]);
357        bytes[HEADER_CRC..HEADER].copy_from_slice(&crc.to_le_bytes());
358    }
359
360    fn table_malformed(result: Result<Vec<u32>, ModuleError>) -> String {
361        match result {
362            Err(ModuleError::Malformed { section: "section table", reason, .. }) => reason,
363            other => panic!("{other:?}"),
364        }
365    }
366
367    #[test]
368    fn a_module_lists_its_sections_and_decodes_them() {
369        let ids = names(&["PAYROLL", "SUB", "PAYROLL"]);
370        let bytes = sample(&ids);
371        let module = Module::read(&bytes).unwrap();
372        assert_eq!(module.version(), Version::CURRENT);
373        let listed: Vec<_> = module.sections().iter().map(|e| (e.id, e.name(), e.optional())).collect();
374        let expected: Vec<_> = Section::ALL.iter().map(|s| (s.id, Some(s.name), false)).collect();
375        assert_eq!(listed, expected);
376        let strings = module.strings().unwrap();
377        assert_eq!(strings.iter().collect::<Vec<_>>(), ["PAYROLL", "SUB"]);
378        let mut r = module.reader(Section::DIRECTORY, &strings).unwrap();
379        assert_eq!(Vec::<String>::decode(&mut r), Ok(ids));
380        assert_eq!(r.finish(), Ok(()));
381        assert_eq!(module.body(Section::DEBUG.id), Ok(&[0u8][..]));
382    }
383
384    #[test]
385    fn the_header_and_table_have_the_documented_layout() {
386        let bytes = sample(&names(&["A"]));
387        assert_eq!(bytes[..8], [0x89, 0x49, 0x57, 0x4D, 0x0D, 0x0A, 0x1A, 0x0A]);
388        assert_eq!(bytes[8..20], [0, 0, 6, 0, 0, 0, 0, 0, 8, 0, 0, 0]);
389        assert_eq!(u64_at(&bytes, 20), Some(bytes.len() as u64));
390        assert_eq!(u32_at(&bytes, HEADER_CRC), Some(extend(crc32(&bytes[..28]), &bytes[32..TABLE_END])));
391        let strings_body = [1, 1, b'A'];
392        assert_eq!(u32_at(&bytes, 32), Some(1));
393        assert_eq!(u32_at(&bytes, 36), Some(0));
394        assert_eq!(u64_at(&bytes, 40), Some(TABLE_END as u64));
395        assert_eq!(u64_at(&bytes, 48), Some(3));
396        assert_eq!(u32_at(&bytes, 56), Some(crc32(&strings_body)));
397        assert_eq!(bytes[TABLE_END..TABLE_END + 3], strings_body);
398        assert_eq!(u64_at(&bytes, 32 + ENTRY + 8), Some(TABLE_END as u64 + 3));
399        assert_eq!(bytes.len(), TABLE_END + 3 + 2 + 6);
400    }
401
402    #[test]
403    fn reading_and_writing_again_gives_the_same_bytes() {
404        let bytes = sample(&names(&["MAIN", "", "SUB", "MAIN"]));
405        assert_eq!(sample(&names(&["MAIN", "", "SUB", "MAIN"])), bytes);
406        let module = Module::read(&bytes).unwrap();
407        let strings = module.strings().unwrap();
408        let mut r = module.reader(Section::DIRECTORY, &strings).unwrap();
409        let decoded = Vec::<String>::decode(&mut r).unwrap();
410        assert_eq!(sample(&decoded), bytes);
411    }
412
413    #[test]
414    fn every_strict_prefix_is_truncated() {
415        let bytes = sample(&names(&["PAYROLL"]));
416        for len in 0..bytes.len() {
417            assert!(matches!(read(&bytes[..len]), Err(ModuleError::Truncated { .. })), "{len}");
418        }
419        assert_eq!(read(&bytes[..100]), Err(ModuleError::Truncated { expected: bytes.len() as u64, actual: 100 }));
420        assert_eq!(read(&bytes[..20]), Err(ModuleError::Truncated { expected: 32, actual: 20 }));
421    }
422
423    #[test]
424    fn a_bad_magic_is_not_a_module() {
425        let bytes = sample(&names(&["A"]));
426        let mut changed = bytes.clone();
427        changed[0] = 0x09;
428        assert_eq!(read(&changed), Err(ModuleError::NotAModule));
429        let text_mode: Vec<u8> = [&bytes[..4], &bytes[5..]].concat();
430        assert_eq!(read(&text_mode), Err(ModuleError::NotAModule));
431        assert_eq!(read(b"IDENTIFICATION DIVISION."), Err(ModuleError::NotAModule));
432        assert_eq!(read(b"\x89IX"), Err(ModuleError::NotAModule));
433        assert_eq!(ModuleError::NotAModule.to_string(), "not an ironwork load module");
434    }
435
436    #[test]
437    fn a_longer_file_has_trailing_bytes() {
438        let mut bytes = sample(&names(&["A"]));
439        let len = bytes.len() as u64;
440        bytes.push(0);
441        assert_eq!(read(&bytes), Err(ModuleError::TrailingBytes { expected: len, actual: len + 1 }));
442    }
443
444    /// A module of `version` and `features` holding every known section, each a zero count, and what `extra` writes.
445    fn stamped(version: Version, features: u32, extra: impl FnOnce(&mut ModuleWriter)) -> Vec<u8> {
446        let mut w = ModuleWriter::new();
447        for section in &Section::ALL[1..] {
448            w.section(*section, |w| w.count(0));
449        }
450        extra(&mut w);
451        w.finish_as(version, features)
452    }
453
454    #[test]
455    fn a_minor_from_the_oldest_readable_is_read_and_an_older_one_or_another_major_refused() {
456        let current = Version { major: 0, minor: 5 };
457        assert_eq!(Module::read(&stamped(current, 0, |_| {})).unwrap().version(), current);
458        for version in [Version { major: 0, minor: 4 }, Version { major: 1, minor: 0 }] {
459            assert_eq!(read(&stamped(version, 0, |_| {})), Err(ModuleError::Version(version)));
460        }
461        assert_eq!(
462            ModuleError::Version(Version { major: 0, minor: 4 }).to_string(),
463            "load module format 0.4; this ironwork reads 0.5 to 0.6. Compile the source again"
464        );
465        assert_eq!(
466            ModuleError::Version(Version { major: 2, minor: 0 }).to_string(),
467            "load module format 2.0; this ironwork reads 0.5 to 0.6. Compile the source again"
468        );
469        let oldest = Version { major: 0, minor: 3 };
470        assert!(!oldest.reads(Version { major: 0, minor: 2 }));
471        assert!(oldest.reads(Version { major: 0, minor: 3 }) && oldest.reads(Version { major: 0, minor: 9 }));
472        assert!(!oldest.reads(Version { major: 1, minor: 3 }));
473        let frozen = Version { major: 1, minor: 0 };
474        assert!(frozen.reads(Version { major: 1, minor: 0 }) && frozen.reads(Version { major: 1, minor: 5 }));
475        assert!(!frozen.reads(Version { major: 2, minor: 0 }) && !frozen.reads(Version { major: 0, minor: 9 }));
476    }
477
478    #[test]
479    fn a_newer_minor_is_read_past_an_unknown_optional_section_and_refused_for_a_required_one_or_a_feature() {
480        let newer = Version { major: 0, minor: 7 };
481        let optional = stamped(newer, 0, |w| w.push(9, OPTIONAL, |w| w.string("NEXT")));
482        let module = Module::read(&optional).unwrap();
483        assert_eq!(module.version(), newer);
484        let last = module.sections()[8];
485        assert_eq!((last.id, last.name(), last.optional()), (9, None, true));
486        assert_eq!(module.strings().unwrap().get(0), Some("NEXT"));
487        assert!(crate::module::read(&optional).unwrap().programs.is_empty());
488        let extension = stamped(newer, 0, |w| w.extension(EXTENSIONS, |w| w.count(1)));
489        assert_eq!(read(&extension), Ok(vec![1, 2, 3, 4, 5, 6, 7, 8, EXTENSIONS]));
490
491        let required = stamped(newer, 0, |w| w.section(Section { id: 9, name: "NEXT" }, |w| w.count(0)));
492        assert_eq!(read(&required), Err(ModuleError::UnknownSection(9)));
493        assert_eq!(read(&stamped(newer, 4, |_| {})), Err(ModuleError::Feature(4)));
494    }
495
496    #[test]
497    fn a_changed_header_or_table_byte_is_a_bad_header_checksum() {
498        let bytes = sample(&names(&["A"]));
499        for at in (12..20).chain(HEADER_CRC..TABLE_END) {
500            let mut changed = bytes.clone();
501            changed[at] ^= 0x10;
502            assert!(matches!(read(&changed), Err(ModuleError::HeaderChecksum { .. })), "byte {at}");
503        }
504        let mut changed = bytes.clone();
505        changed[20] ^= 1;
506        assert!(matches!(read(&changed), Err(ModuleError::Truncated { .. } | ModuleError::TrailingBytes { .. })));
507    }
508
509    #[test]
510    fn a_set_feature_bit_is_refused_by_name() {
511        assert_eq!(read(&assemble(Version::CURRENT, 4, &plain())), Err(ModuleError::Feature(4)));
512        assert_eq!(
513            ModuleError::Feature(4).to_string(),
514            "load module needs features 0x00000004, which this ironwork lacks"
515        );
516    }
517
518    #[test]
519    fn a_changed_section_byte_is_found_when_the_section_is_read() {
520        let bytes = sample(&names(&["A"]));
521        let module = Module::read(&bytes).unwrap();
522        let layout = module.sections()[3];
523        let mut changed = bytes.clone();
524        changed[layout.offset as usize] ^= 0x01;
525        let module = Module::read(&changed).unwrap();
526        let err = module.body(Section::LAYOUT.id).unwrap_err();
527        let computed = crc32(&[1]);
528        assert_eq!(err, ModuleError::SectionChecksum { id: 4, computed, stored: layout.crc });
529        assert_eq!(
530            err.to_string(),
531            format!("section LAYOUT is corrupt (checksum {computed:08X}, expected {:08X})", layout.crc)
532        );
533        assert!(module.body(Section::LIR.id).is_ok());
534        assert!(module.strings().is_ok());
535    }
536
537    #[test]
538    fn an_unknown_section_is_skipped_only_if_optional() {
539        let mut sections = plain();
540        sections.push((9, 0, &[1, 2]));
541        assert_eq!(read(&assemble(Version::CURRENT, 0, &sections)), Err(ModuleError::UnknownSection(9)));
542        assert_eq!(ModuleError::UnknownSection(9).to_string(), "required section 0x9 is unknown to this ironwork");
543
544        let mut sections = plain();
545        sections.push((9, OPTIONAL, &[1, 2]));
546        sections.push((EXTENSIONS + 1, OPTIONAL, &[3]));
547        let bytes = assemble(Version::CURRENT, 0, &sections);
548        assert_eq!(read(&bytes), Ok(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, EXTENSIONS + 1]));
549        let module = Module::read(&bytes).unwrap();
550        assert_eq!(module.sections()[8].name(), None);
551        assert_eq!(module.body(9), Ok(&[1u8, 2][..]));
552
553        let mut w = ModuleWriter::new();
554        for section in &Section::ALL[1..] {
555            w.section(*section, |w| w.count(0));
556        }
557        w.extension(EXTENSIONS, |w| w.string("NOTE"));
558        let module_bytes = w.finish();
559        let module = Module::read(&module_bytes).unwrap();
560        assert!(module.sections()[8].optional());
561        assert_eq!(module.strings().unwrap().get(0), Some("NOTE"));
562    }
563
564    #[test]
565    fn flags_must_match_what_the_reader_knows() {
566        let mut sections = plain();
567        sections[3].1 = OPTIONAL;
568        assert_eq!(
569            table_malformed(read(&assemble(Version::CURRENT, 0, &sections))),
570            "required section LAYOUT is flagged optional"
571        );
572        let mut sections = plain();
573        sections[2].1 = 2;
574        assert_eq!(table_malformed(read(&assemble(Version::CURRENT, 0, &sections))), "section 0x3 has flags 0x2");
575    }
576
577    #[test]
578    fn a_missing_required_section_is_refused() {
579        let sections = &plain()[..7];
580        assert_eq!(read(&assemble(Version::CURRENT, 0, sections)), Err(ModuleError::MissingSection(8)));
581        assert_eq!(ModuleError::MissingSection(8).to_string(), "required section DEBUG is missing");
582        assert_eq!(read(&assemble(Version::CURRENT, 0, &[])), Err(ModuleError::MissingSection(1)));
583        let bytes = sample(&[]);
584        assert_eq!(Module::read(&bytes).unwrap().body(9), Err(ModuleError::MissingSection(9)));
585    }
586
587    #[test]
588    fn only_the_canonical_layout_is_read() {
589        let mut sections = plain();
590        sections.swap(4, 5);
591        assert_eq!(table_malformed(read(&assemble(Version::CURRENT, 0, &sections))), "section 0x5 follows section 0x6");
592        let mut sections = plain();
593        sections.insert(4, (4, 0, &[0]));
594        assert_eq!(table_malformed(read(&assemble(Version::CURRENT, 0, &sections))), "section 0x4 follows section 0x4");
595
596        let bytes = assemble(Version::CURRENT, 0, &plain());
597        let mut gap = bytes.clone();
598        gap[32 + ENTRY + 8] += 1;
599        reseal(&mut gap);
600        assert_eq!(
601            table_malformed(read(&gap)),
602            format!("section 0x2 begins at byte {}, not {}", TABLE_END + 2, TABLE_END + 1)
603        );
604        let mut long = bytes.clone();
605        long[32 + 7 * ENTRY + 16] += 1;
606        reseal(&mut long);
607        assert_eq!(table_malformed(read(&long)), "section 0x8 of 2 bytes runs past the end");
608        let mut short = bytes.clone();
609        short[32 + 7 * ENTRY + 16] -= 1;
610        reseal(&mut short);
611        assert_eq!(
612            table_malformed(read(&short)),
613            format!("the sections end at byte {}, before the end at {}", TABLE_END + 7, TABLE_END + 8)
614        );
615        let mut many = bytes;
616        many[16] = 200;
617        reseal(&mut many);
618        assert_eq!(
619            table_malformed(read(&many)),
620            format!("200 sections need a table to byte {}, past the end at {}", HEADER + 200 * ENTRY, TABLE_END + 8)
621        );
622    }
623
624    #[test]
625    #[should_panic(expected = "section 0x3 written after section 0x4")]
626    fn the_writer_takes_sections_in_id_order() {
627        let mut w = ModuleWriter::new();
628        w.section(Section::LAYOUT, |_| {});
629        w.section(Section::OPTIONS, |_| {});
630    }
631
632    #[test]
633    #[should_panic(expected = "section SQL was not written")]
634    fn the_writer_writes_every_required_section() {
635        let mut w = ModuleWriter::new();
636        for section in
637            [Section::DIRECTORY, Section::OPTIONS, Section::LAYOUT, Section::LIR, Section::BMS, Section::DEBUG]
638        {
639            w.section(section, |w| w.count(0));
640        }
641        w.finish();
642    }
643}