mint_cli/output/
mod.rs

1pub mod args;
2pub mod checksum;
3pub mod errors;
4
5use crate::layout::header::{CrcLocation, Header};
6use crate::layout::settings::{CrcArea, Endianness, Settings};
7use crate::output::args::OutputFormat;
8use errors::OutputError;
9
10use bin_file::{BinFile, IHexFormat};
11
12#[derive(Debug, Clone)]
13pub struct DataRange {
14    pub start_address: u32,
15    pub bytestream: Vec<u8>,
16    pub crc_address: u32,
17    pub crc_bytestream: Vec<u8>,
18    pub used_size: u32,
19    pub allocated_size: u32,
20}
21
22fn byte_swap_inplace(bytes: &mut [u8]) {
23    for chunk in bytes.chunks_exact_mut(2) {
24        chunk.swap(0, 1);
25    }
26}
27
28fn validate_crc_location(length: usize, header: &Header) -> Result<Option<u32>, OutputError> {
29    let crc_offset = match &header.crc_location {
30        CrcLocation::Address(address) => {
31            let crc_offset = address.checked_sub(header.start_address).ok_or_else(|| {
32                OutputError::HexOutputError("CRC address before block start.".to_string())
33            })?;
34
35            if crc_offset < length as u32 {
36                return Err(OutputError::HexOutputError(
37                    "CRC overlaps with payload.".to_string(),
38                ));
39            }
40
41            crc_offset
42        }
43        CrcLocation::Keyword(option) => match option.as_str() {
44            "none" => return Ok(None),
45            "end" => (length as u32 + 3) & !3,
46            _ => {
47                return Err(OutputError::HexOutputError(format!(
48                    "Invalid CRC location: {}",
49                    option
50                )));
51            }
52        },
53    };
54
55    if header.length < crc_offset + 4 {
56        return Err(OutputError::HexOutputError(
57            "CRC location would overrun block.".to_string(),
58        ));
59    }
60
61    Ok(Some(crc_offset))
62}
63
64pub fn bytestream_to_datarange(
65    mut bytestream: Vec<u8>,
66    header: &Header,
67    settings: &Settings,
68    byte_swap: bool,
69    pad_to_end: bool,
70    padding_bytes: u32,
71) -> Result<DataRange, OutputError> {
72    if bytestream.len() > header.length as usize {
73        return Err(OutputError::HexOutputError(
74            "Bytestream length exceeds block length.".to_string(),
75        ));
76    }
77
78    // Apply optional byte swap across the entire stream before CRC
79    if byte_swap {
80        if !bytestream.len().is_multiple_of(2) {
81            bytestream.push(header.padding);
82        }
83        byte_swap_inplace(bytestream.as_mut_slice());
84    }
85
86    // Determine CRC location relative to current payload end
87    let crc_location = validate_crc_location(bytestream.len(), header)?;
88
89    let mut used_size = (bytestream.len() as u32).saturating_sub(padding_bytes);
90    let allocated_size = header.length;
91
92    // If CRC is disabled for this block, return early with no CRC
93    let Some(crc_offset) = crc_location else {
94        if pad_to_end {
95            bytestream.resize(header.length as usize, header.padding);
96        }
97
98        return Ok(DataRange {
99            start_address: header.start_address + settings.virtual_offset,
100            bytestream,
101            crc_address: 0,
102            crc_bytestream: Vec::new(),
103            used_size,
104            allocated_size,
105        });
106    };
107
108    // CRC is enabled - require settings.crc
109    let crc_settings = settings.crc.as_ref().ok_or_else(|| {
110        OutputError::HexOutputError(
111            "CRC location specified but no [settings.crc] defined.".to_string(),
112        )
113    })?;
114
115    used_size = used_size.saturating_add(4);
116
117    // Padding for CRC alignment
118    if let CrcLocation::Keyword(_) = &header.crc_location {
119        bytestream.resize(crc_offset as usize, header.padding);
120    }
121
122    // Handle block-level CRC modes
123    match crc_settings.area {
124        CrcArea::BlockZeroCrc | CrcArea::BlockPadCrc | CrcArea::BlockOmitCrc => {
125            bytestream.resize(header.length as usize, header.padding);
126        }
127        CrcArea::Data => {}
128    }
129
130    // Zero CRC location for BlockZeroCrc mode
131    if crc_settings.area == CrcArea::BlockZeroCrc {
132        bytestream[crc_offset as usize..(crc_offset + 4) as usize].fill(0);
133    }
134
135    // Compute CRC - omit CRC bytes for BlockOmitCrc mode
136    let crc_val = if crc_settings.area == CrcArea::BlockOmitCrc {
137        let before = &bytestream[..crc_offset as usize];
138        let after = &bytestream[(crc_offset + 4) as usize..];
139        let combined: Vec<u8> = [before, after].concat();
140        checksum::calculate_crc(&combined, crc_settings)
141    } else {
142        checksum::calculate_crc(&bytestream, crc_settings)
143    };
144
145    let mut crc_bytes: [u8; 4] = match settings.endianness {
146        Endianness::Big => crc_val.to_be_bytes(),
147        Endianness::Little => crc_val.to_le_bytes(),
148    };
149    if byte_swap {
150        byte_swap_inplace(&mut crc_bytes);
151    }
152
153    // Resize to full block if pad_to_end is true
154    if pad_to_end {
155        bytestream.resize(header.length as usize, header.padding);
156    }
157
158    Ok(DataRange {
159        start_address: header.start_address + settings.virtual_offset,
160        bytestream,
161        crc_address: header.start_address + settings.virtual_offset + crc_offset,
162        crc_bytestream: crc_bytes.to_vec(),
163        used_size,
164        allocated_size,
165    })
166}
167
168pub fn emit_hex(
169    ranges: &[DataRange],
170    record_width: usize,
171    format: OutputFormat,
172) -> Result<String, OutputError> {
173    if !(1..=128).contains(&record_width) {
174        return Err(OutputError::HexOutputError(
175            "Record width must be between 1 and 128".to_string(),
176        ));
177    }
178
179    // Use bin_file to format output.
180    let mut bf = BinFile::new();
181    let mut max_end: usize = 0;
182
183    for range in ranges {
184        bf.add_bytes(
185            range.bytestream.as_slice(),
186            Some(range.start_address as usize),
187            false,
188        )
189        .map_err(|e| OutputError::HexOutputError(format!("Failed to add bytes: {}", e)))?;
190
191        // Only add CRC bytes if CRC is enabled for this block
192        if !range.crc_bytestream.is_empty() {
193            bf.add_bytes(
194                range.crc_bytestream.as_slice(),
195                Some(range.crc_address as usize),
196                true,
197            )
198            .map_err(|e| OutputError::HexOutputError(format!("Failed to add bytes: {}", e)))?;
199        }
200
201        let end = (range.start_address as usize).saturating_add(range.bytestream.len());
202        if end > max_end {
203            max_end = end;
204        }
205        if !range.crc_bytestream.is_empty() {
206            let end = (range.crc_address as usize).saturating_add(range.crc_bytestream.len());
207            if end > max_end {
208                max_end = end;
209            }
210        }
211    }
212
213    match format {
214        OutputFormat::Hex => {
215            let ihex_format = if max_end <= 0x1_0000 {
216                IHexFormat::IHex16
217            } else {
218                IHexFormat::IHex32
219            };
220            let lines = bf.to_ihex(Some(record_width), ihex_format).map_err(|e| {
221                OutputError::HexOutputError(format!("Failed to generate Intel HEX: {}", e))
222            })?;
223            Ok(lines.join("\n"))
224        }
225        OutputFormat::Mot => {
226            use bin_file::SRecordAddressLength;
227            let addr_len = if max_end <= 0x1_0000 {
228                SRecordAddressLength::Length16
229            } else if max_end <= 0x100_0000 {
230                SRecordAddressLength::Length24
231            } else {
232                SRecordAddressLength::Length32
233            };
234            let lines = bf.to_srec(Some(record_width), addr_len).map_err(|e| {
235                OutputError::HexOutputError(format!("Failed to generate S-Record: {}", e))
236            })?;
237            Ok(lines.join("\n"))
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::layout::header::CrcLocation;
246    use crate::layout::header::Header;
247    use crate::layout::settings::Endianness;
248    use crate::layout::settings::Settings;
249    use crate::layout::settings::{CrcArea, CrcData};
250
251    fn sample_crc_data() -> CrcData {
252        CrcData {
253            polynomial: 0x04C11DB7,
254            start: 0xFFFF_FFFF,
255            xor_out: 0xFFFF_FFFF,
256            ref_in: true,
257            ref_out: true,
258            area: CrcArea::Data,
259        }
260    }
261
262    fn sample_settings() -> Settings {
263        Settings {
264            endianness: Endianness::Little,
265            virtual_offset: 0,
266            crc: Some(sample_crc_data()),
267            byte_swap: false,
268            pad_to_end: false,
269        }
270    }
271
272    fn sample_header(len: u32) -> Header {
273        Header {
274            start_address: 0,
275            length: len,
276            crc_location: CrcLocation::Keyword("end".to_string()),
277            padding: 0xFF,
278        }
279    }
280
281    #[test]
282    fn pad_to_end_false_resizes_to_crc_end_only() {
283        let settings = sample_settings();
284        let crc_data = sample_crc_data();
285        let header = sample_header(16);
286
287        let bytestream = vec![1u8, 2, 3, 4];
288        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, false, false, 0)
289            .expect("data range generation failed");
290        let hex = emit_hex(&[dr], 16, crate::output::args::OutputFormat::Hex)
291            .expect("hex generation failed");
292
293        // No in-memory resize when pad_to_end=false; CRC is emitted separately
294        assert_eq!(bytestream.len(), 4);
295
296        // And the emitted hex should contain the CRC bytes (endianness applied)
297        let crc_offset = super::validate_crc_location(4usize, &header)
298            .expect("crc loc")
299            .expect("crc should be enabled");
300        assert_eq!(crc_offset as usize, 4, "crc should follow payload end");
301        let crc_val = checksum::calculate_crc(&bytestream[..crc_offset as usize], &crc_data);
302        let crc_bytes = match settings.endianness {
303            Endianness::Big => crc_val.to_be_bytes(),
304            Endianness::Little => crc_val.to_le_bytes(),
305        };
306        // No byte swap in this test
307        let expected_crc_ascii = crc_bytes
308            .iter()
309            .map(|b| format!("{:02X}", b))
310            .collect::<String>();
311        assert!(
312            hex.to_uppercase().contains(&expected_crc_ascii),
313            "hex should contain CRC bytes"
314        );
315    }
316    #[test]
317    fn pad_to_end_true_resizes_to_full_block() {
318        let settings = sample_settings();
319        let header = sample_header(32);
320
321        let bytestream = vec![1u8, 2, 3, 4];
322        let dr = bytestream_to_datarange(bytestream, &header, &settings, false, true, 0)
323            .expect("data range generation failed");
324
325        assert_eq!(dr.bytestream.len(), header.length as usize);
326    }
327
328    #[test]
329    fn block_zero_crc_zeros_crc_location() {
330        let mut crc_data = sample_crc_data();
331        crc_data.area = CrcArea::BlockZeroCrc;
332        let settings = Settings {
333            crc: Some(crc_data),
334            ..sample_settings()
335        };
336        let header = sample_header(32);
337
338        let bytestream = vec![1u8, 2, 3, 4];
339        let dr = bytestream_to_datarange(bytestream, &header, &settings, false, false, 0)
340            .expect("data range generation failed");
341
342        assert_eq!(dr.bytestream.len(), header.length as usize);
343        let crc_offset = validate_crc_location(4usize, &header)
344            .expect("crc loc")
345            .expect("crc enabled");
346        assert_eq!(
347            dr.bytestream[crc_offset as usize..(crc_offset + 4) as usize],
348            [0, 0, 0, 0],
349            "CRC location should be zeroed"
350        );
351    }
352
353    #[test]
354    fn block_pad_crc_includes_padding_at_crc_location() {
355        let mut crc_data = sample_crc_data();
356        crc_data.area = CrcArea::BlockPadCrc;
357        let settings = Settings {
358            crc: Some(crc_data),
359            ..sample_settings()
360        };
361        let header = sample_header(32);
362
363        let bytestream = vec![1u8, 2, 3, 4];
364        let dr = bytestream_to_datarange(bytestream, &header, &settings, false, false, 0)
365            .expect("data range generation failed");
366
367        assert_eq!(dr.bytestream.len(), header.length as usize);
368        let crc_offset = validate_crc_location(4usize, &header)
369            .expect("crc loc")
370            .expect("crc enabled");
371        assert_eq!(
372            dr.bytestream[crc_offset as usize..(crc_offset + 4) as usize],
373            [0xFF, 0xFF, 0xFF, 0xFF],
374            "CRC location should contain padding value"
375        );
376    }
377
378    #[test]
379    fn block_omit_crc_excludes_crc_bytes_from_calculation() {
380        let mut crc_data = sample_crc_data();
381        crc_data.area = CrcArea::BlockOmitCrc;
382        let settings = Settings {
383            crc: Some(crc_data.clone()),
384            ..sample_settings()
385        };
386        let header = sample_header(32);
387
388        let bytestream = vec![1u8, 2, 3, 4];
389        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, false, false, 0)
390            .expect("data range generation failed");
391
392        assert_eq!(dr.bytestream.len(), header.length as usize);
393        let crc_offset = validate_crc_location(4usize, &header)
394            .expect("crc loc")
395            .expect("crc enabled");
396
397        // Calculate expected CRC by omitting CRC bytes (same logic as in bytestream_to_datarange)
398        let before = &dr.bytestream[..crc_offset as usize];
399        let after = &dr.bytestream[(crc_offset + 4) as usize..];
400        let combined: Vec<u8> = [before, after].concat();
401        let expected_crc = checksum::calculate_crc(&combined, &crc_data);
402
403        // Extract actual CRC from the result (accounting for endianness)
404        let actual_crc = match settings.endianness {
405            Endianness::Little => u32::from_le_bytes(
406                dr.crc_bytestream[..4]
407                    .try_into()
408                    .expect("CRC bytes should be 4 bytes"),
409            ),
410            Endianness::Big => u32::from_be_bytes(
411                dr.crc_bytestream[..4]
412                    .try_into()
413                    .expect("CRC bytes should be 4 bytes"),
414            ),
415        };
416
417        assert_eq!(
418            expected_crc, actual_crc,
419            "CRC should match calculation with CRC bytes omitted"
420        );
421
422        // Verify that including CRC bytes produces a different result
423        let crc_with_bytes = checksum::calculate_crc(&dr.bytestream, &crc_data);
424        assert_ne!(
425            expected_crc, crc_with_bytes,
426            "CRC with bytes included should differ from CRC with bytes omitted"
427        );
428    }
429
430    #[test]
431    fn crc_location_none_skips_crc() {
432        let settings = Settings {
433            crc: None,
434            ..sample_settings()
435        };
436        let header = Header {
437            crc_location: CrcLocation::Keyword("none".to_string()),
438            ..sample_header(32)
439        };
440
441        let bytestream = vec![1u8, 2, 3, 4];
442        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, false, false, 0)
443            .expect("data range generation failed");
444
445        assert!(dr.crc_bytestream.is_empty(), "CRC should be empty");
446        assert_eq!(dr.crc_address, 0, "CRC address should be 0");
447        assert_eq!(dr.bytestream.len(), 4, "bytestream should not be padded");
448    }
449
450    #[test]
451    fn crc_location_none_with_pad_to_end() {
452        let settings = Settings {
453            crc: None,
454            ..sample_settings()
455        };
456        let header = Header {
457            crc_location: CrcLocation::Keyword("none".to_string()),
458            ..sample_header(32)
459        };
460
461        let bytestream = vec![1u8, 2, 3, 4];
462        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, false, true, 0)
463            .expect("data range generation failed");
464
465        assert!(dr.crc_bytestream.is_empty(), "CRC should be empty");
466        assert_eq!(
467            dr.bytestream.len(),
468            32,
469            "bytestream should be padded to full block"
470        );
471    }
472
473    #[test]
474    fn crc_required_but_settings_missing_errors() {
475        let settings = Settings {
476            crc: None,
477            ..sample_settings()
478        };
479        let header = sample_header(32); // uses crc_location = "end"
480
481        let bytestream = vec![1u8, 2, 3, 4];
482        let result = bytestream_to_datarange(bytestream, &header, &settings, false, false, 0);
483
484        assert!(result.is_err());
485        assert!(result
486            .unwrap_err()
487            .to_string()
488            .contains("no [settings.crc] defined"));
489    }
490}