Skip to main content

mint_cli/output/
mod.rs

1pub mod args;
2pub mod checksum;
3pub mod error;
4pub mod report;
5
6use crate::layout::header::Header;
7use crate::layout::settings::{CrcArea, CrcConfig, CrcLocation, Endianness, Settings};
8use crate::output::args::OutputFormat;
9use error::OutputError;
10
11use bin_file::{BinFile, IHexFormat};
12
13/// Swaps bytes pairwise for word-addressing mode.
14fn byte_swap_inplace(bytes: &mut [u8]) {
15    for chunk in bytes.chunks_exact_mut(2) {
16        chunk.swap(0, 1);
17    }
18}
19
20#[derive(Debug, Clone)]
21pub struct DataRange {
22    pub start_address: u32,
23    pub bytestream: Vec<u8>,
24    pub crc_address: u32,
25    pub crc_bytestream: Vec<u8>,
26    pub used_size: u32,
27    pub allocated_size: u32,
28}
29
30/// Resolves CRC config from header + settings, validates location, returns offset + config.
31fn resolve_crc(
32    length: usize,
33    header: &Header,
34    settings: &Settings,
35    block_len_bytes: u32,
36) -> Result<Option<(u32, CrcConfig)>, OutputError> {
37    // Merge header CRC with settings CRC
38    let resolved = header
39        .crc
40        .as_ref()
41        .map(|hc| hc.resolve(settings.crc.as_ref()))
42        .unwrap_or_else(|| settings.crc.clone().unwrap_or_default());
43
44    // Check if CRC is disabled
45    if resolved.is_disabled() {
46        return Ok(None);
47    }
48
49    let location = resolved.location.as_ref().ok_or_else(|| {
50        OutputError::HexOutputError("CRC enabled but no location specified.".to_string())
51    })?;
52
53    // Absolute addresses must come from header, not settings
54    if let CrcLocation::Address(_) = location {
55        let header_has_location = header.crc.as_ref().is_some_and(|hc| hc.location.is_some());
56        if !header_has_location {
57            return Err(OutputError::HexOutputError(
58                "Absolute CRC address not allowed in [settings.crc]; use [header.crc] instead."
59                    .to_string(),
60            ));
61        }
62    }
63
64    let crc_offset = match location {
65        CrcLocation::Address(address) => {
66            let raw_offset = address.checked_sub(header.start_address).ok_or_else(|| {
67                OutputError::HexOutputError("CRC address before block start.".to_string())
68            })?;
69            let crc_offset = if settings.word_addressing {
70                raw_offset.checked_mul(2).ok_or_else(|| {
71                    OutputError::HexOutputError("CRC address overflows block length.".to_string())
72                })?
73            } else {
74                raw_offset
75            };
76
77            if crc_offset < length as u32 {
78                return Err(OutputError::HexOutputError(
79                    "CRC overlaps with payload.".to_string(),
80                ));
81            }
82
83            crc_offset
84        }
85        CrcLocation::Keyword(option) => match option.as_str() {
86            "end_data" => (length as u32 + 3) & !3,
87            "end_block" => {
88                let offset = block_len_bytes.saturating_sub(4);
89                if offset < length as u32 {
90                    return Err(OutputError::HexOutputError(
91                        "CRC at end_block overlaps with payload data.".to_string(),
92                    ));
93                }
94                offset
95            }
96            _ => {
97                return Err(OutputError::HexOutputError(format!(
98                    "Invalid CRC location: '{}'. Use 'end_data', 'end_block', or an address.",
99                    option
100                )));
101            }
102        },
103    };
104
105    if block_len_bytes < crc_offset + 4 {
106        return Err(OutputError::HexOutputError(
107            "CRC location would overrun block.".to_string(),
108        ));
109    }
110
111    // Verify all CRC parameters are present
112    if !resolved.is_complete() {
113        return Err(OutputError::HexOutputError(
114            "CRC location specified but missing CRC parameters (polynomial, start, etc)."
115                .to_string(),
116        ));
117    }
118
119    Ok(Some((crc_offset, resolved)))
120}
121
122pub fn bytestream_to_datarange(
123    mut bytestream: Vec<u8>,
124    header: &Header,
125    settings: &Settings,
126    padding_bytes: u32,
127) -> Result<DataRange, OutputError> {
128    let addr_mult: u32 = if settings.word_addressing { 2 } else { 1 };
129    let block_len_bytes = header.length.checked_mul(addr_mult).ok_or_else(|| {
130        OutputError::HexOutputError("Block length overflows address space.".to_string())
131    })?;
132
133    if bytestream.len() > block_len_bytes as usize {
134        return Err(OutputError::HexOutputError(
135            "Bytestream length exceeds block length.".to_string(),
136        ));
137    }
138
139    // Apply byte swap for word-addressing mode BEFORE CRC calculation
140    if settings.word_addressing {
141        if !bytestream.len().is_multiple_of(2) {
142            bytestream.push(header.padding);
143        }
144        byte_swap_inplace(&mut bytestream);
145    }
146
147    // Resolve CRC configuration (location + settings) from header + global defaults
148    let crc_config = resolve_crc(bytestream.len(), header, settings, block_len_bytes)?;
149
150    let mut used_size = (bytestream.len() as u32).saturating_sub(padding_bytes);
151
152    // If CRC is disabled for this block, return early with no CRC
153    let Some((crc_offset, crc_settings)) = crc_config else {
154        return Ok(DataRange {
155            start_address: header.start_address * addr_mult + settings.virtual_offset,
156            bytestream,
157            crc_address: 0,
158            crc_bytestream: Vec::new(),
159            used_size,
160            allocated_size: block_len_bytes,
161        });
162    };
163
164    used_size = used_size.saturating_add(4);
165
166    let area = crc_settings.area.unwrap(); // Safe: is_complete() verified
167    let is_end_block = matches!(
168        &crc_settings.location,
169        Some(CrcLocation::Keyword(kw)) if kw == "end_block"
170    );
171
172    // Prepare bytestream and compute CRC based on area
173    let crc_val = match area {
174        CrcArea::Data => {
175            // For end_data: pad to crc_offset before CRC calculation (aligning the CRC to be appended to the struct)
176            // For end_block: CRC over raw data, pad afterwards
177            if !is_end_block {
178                bytestream.resize(crc_offset as usize, header.padding);
179            }
180            let crc = checksum::calculate_crc(&bytestream, &crc_settings);
181            if is_end_block {
182                bytestream.resize(crc_offset as usize, header.padding);
183            }
184            crc
185        }
186        CrcArea::BlockZeroCrc => {
187            // Pad to full block, zero CRC location, then calculate
188            bytestream.resize(block_len_bytes as usize, header.padding);
189            bytestream[crc_offset as usize..(crc_offset + 4) as usize].fill(0);
190            checksum::calculate_crc(&bytestream, &crc_settings)
191        }
192        CrcArea::BlockPadCrc => {
193            // Pad to full block (CRC location contains padding), then calculate
194            bytestream.resize(block_len_bytes as usize, header.padding);
195            checksum::calculate_crc(&bytestream, &crc_settings)
196        }
197        CrcArea::BlockOmitCrc => {
198            // Pad to full block, calculate CRC excluding CRC bytes
199            bytestream.resize(block_len_bytes as usize, header.padding);
200            let before = &bytestream[..crc_offset as usize];
201            let after = &bytestream[(crc_offset + 4) as usize..];
202            let combined: Vec<u8> = [before, after].concat();
203            checksum::calculate_crc(&combined, &crc_settings)
204        }
205    };
206
207    let mut crc_bytes: [u8; 4] = match settings.endianness {
208        Endianness::Big => crc_val.to_be_bytes(),
209        Endianness::Little => crc_val.to_le_bytes(),
210    };
211
212    // Swap CRC bytes for word-addressing mode (bytestream already swapped above)
213    if settings.word_addressing {
214        byte_swap_inplace(&mut crc_bytes);
215    }
216
217    let start_address = header.start_address * addr_mult + settings.virtual_offset;
218
219    Ok(DataRange {
220        start_address,
221        bytestream,
222        crc_address: start_address + crc_offset,
223        crc_bytestream: crc_bytes.to_vec(),
224        used_size,
225        allocated_size: block_len_bytes,
226    })
227}
228
229pub fn emit_hex(
230    ranges: &[DataRange],
231    record_width: usize,
232    format: OutputFormat,
233) -> Result<String, OutputError> {
234    if !(1..=128).contains(&record_width) {
235        return Err(OutputError::HexOutputError(
236            "Record width must be between 1 and 128".to_string(),
237        ));
238    }
239
240    // Use bin_file to format output.
241    let mut bf = BinFile::new();
242    let mut max_end: usize = 0;
243
244    for range in ranges {
245        bf.add_bytes(
246            range.bytestream.as_slice(),
247            Some(range.start_address as usize),
248            false,
249        )
250        .map_err(|e| OutputError::HexOutputError(format!("Failed to add bytes: {}", e)))?;
251
252        // Only add CRC bytes if CRC is enabled for this block
253        if !range.crc_bytestream.is_empty() {
254            bf.add_bytes(
255                range.crc_bytestream.as_slice(),
256                Some(range.crc_address as usize),
257                true,
258            )
259            .map_err(|e| OutputError::HexOutputError(format!("Failed to add bytes: {}", e)))?;
260        }
261
262        let end = (range.start_address as usize).saturating_add(range.bytestream.len());
263        if end > max_end {
264            max_end = end;
265        }
266        if !range.crc_bytestream.is_empty() {
267            let end = (range.crc_address as usize).saturating_add(range.crc_bytestream.len());
268            if end > max_end {
269                max_end = end;
270            }
271        }
272    }
273
274    match format {
275        OutputFormat::Hex => {
276            let ihex_format = if max_end <= 0x1_0000 {
277                IHexFormat::IHex16
278            } else {
279                IHexFormat::IHex32
280            };
281            let lines = bf.to_ihex(Some(record_width), ihex_format).map_err(|e| {
282                OutputError::HexOutputError(format!("Failed to generate Intel HEX: {}", e))
283            })?;
284            Ok(lines.join("\n"))
285        }
286        OutputFormat::Mot => {
287            use bin_file::SRecordAddressLength;
288            let addr_len = if max_end <= 0x1_0000 {
289                SRecordAddressLength::Length16
290            } else if max_end <= 0x100_0000 {
291                SRecordAddressLength::Length24
292            } else {
293                SRecordAddressLength::Length32
294            };
295            let lines = bf.to_srec(Some(record_width), addr_len).map_err(|e| {
296                OutputError::HexOutputError(format!("Failed to generate S-Record: {}", e))
297            })?;
298            Ok(lines.join("\n"))
299        }
300    }
301}
302
303/// Represents an output file to be written.
304#[derive(Debug, Clone)]
305pub struct OutputFile {
306    pub ranges: Vec<DataRange>,
307    pub format: OutputFormat,
308    pub record_width: usize,
309}
310
311impl OutputFile {
312    /// Render this file's contents as a hex/mot string.
313    pub fn render(&self) -> Result<String, OutputError> {
314        emit_hex(&self.ranges, self.record_width, self.format)
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::layout::header::Header;
322    use crate::layout::settings::Endianness;
323    use crate::layout::settings::Settings;
324    use crate::layout::settings::{CrcArea, CrcConfig, CrcLocation};
325
326    fn sample_crc_config() -> CrcConfig {
327        CrcConfig {
328            location: Some(CrcLocation::Keyword("end_data".to_string())),
329            polynomial: Some(0x04C11DB7),
330            start: Some(0xFFFF_FFFF),
331            xor_out: Some(0xFFFF_FFFF),
332            ref_in: Some(true),
333            ref_out: Some(true),
334            area: Some(CrcArea::Data),
335        }
336    }
337
338    fn sample_settings() -> Settings {
339        Settings {
340            endianness: Endianness::Little,
341            virtual_offset: 0,
342            word_addressing: false,
343            crc: Some(sample_crc_config()),
344        }
345    }
346
347    fn sample_header(len: u32) -> Header {
348        Header {
349            start_address: 0,
350            length: len,
351            crc: Some(CrcConfig {
352                location: Some(CrcLocation::Keyword("end_data".to_string())),
353                ..Default::default()
354            }),
355            padding: 0xFF,
356        }
357    }
358
359    fn header_no_crc(len: u32) -> Header {
360        Header {
361            start_address: 0,
362            length: len,
363            crc: None,
364            padding: 0xFF,
365        }
366    }
367
368    #[test]
369    fn pad_to_end_false_resizes_to_crc_end_only() {
370        let settings = sample_settings();
371        let crc_config = sample_crc_config();
372        let header = sample_header(16);
373
374        let bytestream = vec![1u8, 2, 3, 4];
375        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, 0)
376            .expect("data range generation failed");
377        let hex = emit_hex(&[dr], 16, crate::output::args::OutputFormat::Hex)
378            .expect("hex generation failed");
379
380        // No in-memory resize when pad_to_end=false; CRC is emitted separately
381        assert_eq!(bytestream.len(), 4);
382
383        // CRC offset should be 4 (aligned to 4-byte boundary after payload)
384        let crc_val = checksum::calculate_crc(&bytestream[..4], &crc_config);
385        let crc_bytes = match settings.endianness {
386            Endianness::Big => crc_val.to_be_bytes(),
387            Endianness::Little => crc_val.to_le_bytes(),
388        };
389        let expected_crc_ascii = crc_bytes
390            .iter()
391            .map(|b| format!("{:02X}", b))
392            .collect::<String>();
393        assert!(
394            hex.to_uppercase().contains(&expected_crc_ascii),
395            "hex should contain CRC bytes"
396        );
397    }
398
399    #[test]
400    fn block_zero_crc_zeros_crc_location() {
401        let mut crc_config = sample_crc_config();
402        crc_config.area = Some(CrcArea::BlockZeroCrc);
403        let settings = Settings {
404            crc: Some(crc_config),
405            ..sample_settings()
406        };
407        let header = sample_header(32);
408
409        let bytestream = vec![1u8, 2, 3, 4];
410        let dr = bytestream_to_datarange(bytestream, &header, &settings, 0)
411            .expect("data range generation failed");
412
413        assert_eq!(dr.bytestream.len(), header.length as usize);
414        let crc_offset = 4u32;
415        assert_eq!(
416            dr.bytestream[crc_offset as usize..(crc_offset + 4) as usize],
417            [0, 0, 0, 0],
418            "CRC location should be zeroed"
419        );
420    }
421
422    #[test]
423    fn block_pad_crc_includes_padding_at_crc_location() {
424        let mut crc_config = sample_crc_config();
425        crc_config.area = Some(CrcArea::BlockPadCrc);
426        let settings = Settings {
427            crc: Some(crc_config),
428            ..sample_settings()
429        };
430        let header = sample_header(32);
431
432        let bytestream = vec![1u8, 2, 3, 4];
433        let dr = bytestream_to_datarange(bytestream, &header, &settings, 0)
434            .expect("data range generation failed");
435
436        assert_eq!(dr.bytestream.len(), header.length as usize);
437        let crc_offset = 4u32;
438        assert_eq!(
439            dr.bytestream[crc_offset as usize..(crc_offset + 4) as usize],
440            [0xFF, 0xFF, 0xFF, 0xFF],
441            "CRC location should contain padding value"
442        );
443    }
444
445    #[test]
446    fn block_omit_crc_excludes_crc_bytes_from_calculation() {
447        let mut crc_config = sample_crc_config();
448        crc_config.area = Some(CrcArea::BlockOmitCrc);
449        let settings = Settings {
450            crc: Some(crc_config.clone()),
451            ..sample_settings()
452        };
453        let header = sample_header(32);
454
455        let bytestream = vec![1u8, 2, 3, 4];
456        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, 0)
457            .expect("data range generation failed");
458
459        assert_eq!(dr.bytestream.len(), header.length as usize);
460        let crc_offset = 4u32;
461
462        // Calculate expected CRC by omitting CRC bytes
463        let before = &dr.bytestream[..crc_offset as usize];
464        let after = &dr.bytestream[(crc_offset + 4) as usize..];
465        let combined: Vec<u8> = [before, after].concat();
466        let expected_crc = checksum::calculate_crc(&combined, &crc_config);
467
468        // Extract actual CRC from the result
469        let actual_crc = match settings.endianness {
470            Endianness::Little => u32::from_le_bytes(
471                dr.crc_bytestream[..4]
472                    .try_into()
473                    .expect("CRC bytes should be 4 bytes"),
474            ),
475            Endianness::Big => u32::from_be_bytes(
476                dr.crc_bytestream[..4]
477                    .try_into()
478                    .expect("CRC bytes should be 4 bytes"),
479            ),
480        };
481
482        assert_eq!(
483            expected_crc, actual_crc,
484            "CRC should match calculation with CRC bytes omitted"
485        );
486
487        // Verify that including CRC bytes produces a different result
488        let crc_with_bytes = checksum::calculate_crc(&dr.bytestream, &crc_config);
489        assert_ne!(
490            expected_crc, crc_with_bytes,
491            "CRC with bytes included should differ from CRC with bytes omitted"
492        );
493    }
494
495    #[test]
496    fn no_crc_config_skips_crc() {
497        let settings = Settings {
498            crc: None,
499            ..sample_settings()
500        };
501        let header = header_no_crc(32);
502
503        let bytestream = vec![1u8, 2, 3, 4];
504        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, 0)
505            .expect("data range generation failed");
506
507        assert!(dr.crc_bytestream.is_empty(), "CRC should be empty");
508        assert_eq!(dr.crc_address, 0, "CRC address should be 0");
509        assert_eq!(dr.bytestream.len(), 4, "bytestream should not be padded");
510    }
511
512    #[test]
513    fn end_block_places_crc_at_block_end() {
514        let settings = sample_settings();
515        let header = Header {
516            crc: Some(CrcConfig {
517                location: Some(CrcLocation::Keyword("end_block".to_string())),
518                ..Default::default()
519            }),
520            ..sample_header(32)
521        };
522
523        let bytestream = vec![1u8, 2, 3, 4];
524        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, 0)
525            .expect("data range generation failed");
526
527        // CRC should be at offset 28 (block length 32 - 4)
528        assert_eq!(dr.crc_address, 28);
529        assert!(!dr.crc_bytestream.is_empty());
530    }
531
532    #[test]
533    fn crc_location_set_but_settings_missing_errors() {
534        let settings = Settings {
535            crc: None,
536            ..sample_settings()
537        };
538        // Header has CRC location but no param overrides, and no global settings
539        let header = sample_header(32);
540
541        let bytestream = vec![1u8, 2, 3, 4];
542        let result = bytestream_to_datarange(bytestream, &header, &settings, 0);
543
544        assert!(result.is_err());
545        assert!(
546            result
547                .unwrap_err()
548                .to_string()
549                .contains("missing CRC parameters")
550        );
551    }
552
553    #[test]
554    fn header_crc_overrides_global_settings() {
555        let settings = sample_settings();
556
557        // Header overrides polynomial
558        let header = Header {
559            crc: Some(CrcConfig {
560                location: Some(CrcLocation::Keyword("end_data".to_string())),
561                polynomial: Some(0x1EDC6F41), // Different polynomial
562                ..Default::default()
563            }),
564            ..sample_header(32)
565        };
566
567        let bytestream = vec![1u8, 2, 3, 4];
568        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, 0)
569            .expect("data range generation failed");
570
571        // CRC should be computed with the overridden polynomial
572        let mut expected_config = sample_crc_config();
573        expected_config.polynomial = Some(0x1EDC6F41);
574        let expected_crc = checksum::calculate_crc(&bytestream, &expected_config);
575        let actual_crc = u32::from_le_bytes(dr.crc_bytestream[..4].try_into().unwrap());
576        assert_eq!(expected_crc, actual_crc);
577    }
578
579    #[test]
580    fn header_crc_fully_specified_no_global() {
581        // No global CRC settings
582        let settings = Settings {
583            crc: None,
584            ..sample_settings()
585        };
586
587        // Header fully specifies all CRC settings
588        let header = Header {
589            crc: Some(sample_crc_config()),
590            ..sample_header(32)
591        };
592
593        let bytestream = vec![1u8, 2, 3, 4];
594        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, 0)
595            .expect("data range generation failed");
596
597        // Should succeed and produce a valid CRC
598        assert!(!dr.crc_bytestream.is_empty());
599        let expected_crc = checksum::calculate_crc(&bytestream, &sample_crc_config());
600        let actual_crc = u32::from_le_bytes(dr.crc_bytestream[..4].try_into().unwrap());
601        assert_eq!(expected_crc, actual_crc);
602    }
603
604    #[test]
605    fn settings_location_end_with_header_inheriting() {
606        // Settings specifies location = "end_data" as default
607        let settings = Settings {
608            crc: Some(sample_crc_config()),
609            ..sample_settings()
610        };
611
612        // Header has no crc section - should inherit from settings
613        let header = header_no_crc(32);
614
615        let bytestream = vec![1u8, 2, 3, 4];
616        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, 0)
617            .expect("data range generation failed");
618
619        // Should use CRC from settings
620        assert!(!dr.crc_bytestream.is_empty());
621    }
622
623    #[test]
624    fn settings_absolute_address_rejected() {
625        // Settings with absolute address - should be rejected
626        let settings = Settings {
627            crc: Some(CrcConfig {
628                location: Some(CrcLocation::Address(0x1000)),
629                ..sample_crc_config()
630            }),
631            ..sample_settings()
632        };
633
634        // Header has no crc section - inherits from settings
635        let header = header_no_crc(32);
636
637        let bytestream = vec![1u8, 2, 3, 4];
638        let result = bytestream_to_datarange(bytestream, &header, &settings, 0);
639
640        assert!(result.is_err());
641        assert!(
642            result
643                .unwrap_err()
644                .to_string()
645                .contains("Absolute CRC address not allowed in [settings.crc]")
646        );
647    }
648
649    #[test]
650    fn header_absolute_address_allowed() {
651        let settings = sample_settings();
652
653        // Header specifies absolute address - should work
654        let header = Header {
655            start_address: 0,
656            length: 32,
657            crc: Some(CrcConfig {
658                location: Some(CrcLocation::Address(28)),
659                ..Default::default()
660            }),
661            padding: 0xFF,
662        };
663
664        let bytestream = vec![1u8, 2, 3, 4];
665        let dr = bytestream_to_datarange(bytestream, &header, &settings, 0)
666            .expect("data range generation failed");
667
668        assert_eq!(dr.crc_address, 28);
669        assert!(!dr.crc_bytestream.is_empty());
670    }
671
672    #[test]
673    fn end_block_overlap_with_data_errors() {
674        let settings = sample_settings();
675
676        // Block length is 16, CRC at end_block means offset 12
677        // But data is 16 bytes, which would overlap
678        let header = Header {
679            start_address: 0,
680            length: 16,
681            crc: Some(CrcConfig {
682                location: Some(CrcLocation::Keyword("end_block".to_string())),
683                ..Default::default()
684            }),
685            padding: 0xFF,
686        };
687
688        let bytestream = vec![1u8; 16]; // Data fills entire block
689        let result = bytestream_to_datarange(bytestream, &header, &settings, 0);
690
691        assert!(result.is_err());
692        assert!(
693            result
694                .unwrap_err()
695                .to_string()
696                .contains("overlaps with payload")
697        );
698    }
699}