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<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            "end" => (length as u32 + 3) & !3,
45            _ => {
46                return Err(OutputError::HexOutputError(format!(
47                    "Invalid CRC location: {}",
48                    option
49                )));
50            }
51        },
52    };
53
54    if header.length < crc_offset + 4 {
55        return Err(OutputError::HexOutputError(
56            "CRC location would overrun block.".to_string(),
57        ));
58    }
59
60    Ok(crc_offset)
61}
62
63pub fn bytestream_to_datarange(
64    mut bytestream: Vec<u8>,
65    header: &Header,
66    settings: &Settings,
67    byte_swap: bool,
68    pad_to_end: bool,
69    padding_bytes: u32,
70) -> Result<DataRange, OutputError> {
71    if bytestream.len() > header.length as usize {
72        return Err(OutputError::HexOutputError(
73            "Bytestream length exceeds block length.".to_string(),
74        ));
75    }
76
77    // Apply optional byte swap across the entire stream before CRC
78    if byte_swap {
79        if bytestream.len() % 2 != 0 {
80            bytestream.push(header.padding);
81        }
82        byte_swap_inplace(bytestream.as_mut_slice());
83    }
84
85    // Determine CRC location relative to current payload end
86    let crc_location = validate_crc_location(bytestream.len(), header)?;
87
88    let used_size = ((bytestream.len() as u32).saturating_add(4)).saturating_sub(padding_bytes);
89    let allocated_size = header.length;
90
91    // Padding for CRC alignment
92    if let CrcLocation::Keyword(_) = &header.crc_location {
93        bytestream.resize(crc_location as usize, header.padding);
94    }
95
96    // Fill whole block if the CRC area is block
97    if settings.crc.area == CrcArea::Block {
98        bytestream.resize(header.length as usize, header.padding);
99        bytestream[crc_location as usize..(crc_location + 4) as usize].fill(0);
100    }
101
102    // Compute CRC based on selected area
103    let crc_val = checksum::calculate_crc(&bytestream, &settings.crc);
104
105    let mut crc_bytes: [u8; 4] = match settings.endianness {
106        Endianness::Big => crc_val.to_be_bytes(),
107        Endianness::Little => crc_val.to_le_bytes(),
108    };
109    if byte_swap {
110        byte_swap_inplace(&mut crc_bytes);
111    }
112
113    // Resize to full block if pad_to_end is true
114    if pad_to_end {
115        bytestream.resize(header.length as usize, header.padding);
116    }
117
118    Ok(DataRange {
119        start_address: header.start_address + settings.virtual_offset,
120        bytestream,
121        crc_address: header.start_address + settings.virtual_offset + crc_location,
122        crc_bytestream: crc_bytes.to_vec(),
123        used_size,
124        allocated_size,
125    })
126}
127
128pub fn emit_hex(
129    ranges: &[DataRange],
130    record_width: usize,
131    format: OutputFormat,
132) -> Result<String, OutputError> {
133    if !(1..=128).contains(&record_width) {
134        return Err(OutputError::HexOutputError(
135            "Record width must be between 1 and 128".to_string(),
136        ));
137    }
138
139    // Use bin_file to format output.
140    let mut bf = BinFile::new();
141    let mut max_end: usize = 0;
142
143    for range in ranges {
144        bf.add_bytes(
145            range.bytestream.as_slice(),
146            Some(range.start_address as usize),
147            false,
148        )
149        .map_err(|e| OutputError::HexOutputError(format!("Failed to add bytes: {}", e)))?;
150        bf.add_bytes(
151            range.crc_bytestream.as_slice(),
152            Some(range.crc_address as usize),
153            true,
154        )
155        .map_err(|e| OutputError::HexOutputError(format!("Failed to add bytes: {}", e)))?;
156
157        let end = (range.start_address as usize).saturating_add(range.bytestream.len());
158        if end > max_end {
159            max_end = end;
160        }
161        let end = (range.crc_address as usize).saturating_add(range.crc_bytestream.len());
162        if end > max_end {
163            max_end = end;
164        }
165    }
166
167    match format {
168        OutputFormat::Hex => {
169            let ihex_format = if max_end <= 0x1_0000 {
170                IHexFormat::IHex16
171            } else {
172                IHexFormat::IHex32
173            };
174            let lines = bf.to_ihex(Some(record_width), ihex_format).map_err(|e| {
175                OutputError::HexOutputError(format!("Failed to generate Intel HEX: {}", e))
176            })?;
177            Ok(lines.join("\n"))
178        }
179        OutputFormat::Mot => {
180            use bin_file::SRecordAddressLength;
181            let addr_len = if max_end <= 0x1_0000 {
182                SRecordAddressLength::Length16
183            } else if max_end <= 0x100_0000 {
184                SRecordAddressLength::Length24
185            } else {
186                SRecordAddressLength::Length32
187            };
188            let lines = bf.to_srec(Some(record_width), addr_len).map_err(|e| {
189                OutputError::HexOutputError(format!("Failed to generate S-Record: {}", e))
190            })?;
191            Ok(lines.join("\n"))
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::layout::header::CrcLocation;
200    use crate::layout::header::Header;
201    use crate::layout::settings::Endianness;
202    use crate::layout::settings::Settings;
203    use crate::layout::settings::{CrcArea, CrcData};
204
205    fn sample_settings() -> Settings {
206        Settings {
207            endianness: Endianness::Little,
208            virtual_offset: 0,
209            crc: CrcData {
210                polynomial: 0x04C11DB7,
211                start: 0xFFFF_FFFF,
212                xor_out: 0xFFFF_FFFF,
213                ref_in: true,
214                ref_out: true,
215                area: CrcArea::Data,
216            },
217            byte_swap: false,
218            pad_to_end: false,
219        }
220    }
221
222    fn sample_header(len: u32) -> Header {
223        Header {
224            start_address: 0,
225            length: len,
226            crc_location: CrcLocation::Keyword("end".to_string()),
227            padding: 0xFF,
228        }
229    }
230
231    #[test]
232    fn pad_to_end_false_resizes_to_crc_end_only() {
233        let settings = sample_settings();
234        let header = sample_header(16);
235
236        let bytestream = vec![1u8, 2, 3, 4];
237        let dr = bytestream_to_datarange(bytestream.clone(), &header, &settings, false, false, 0)
238            .expect("data range generation failed");
239        let hex = emit_hex(&[dr], 16, crate::output::args::OutputFormat::Hex)
240            .expect("hex generation failed");
241
242        // No in-memory resize when pad_to_end=false; CRC is emitted separately
243        assert_eq!(bytestream.len(), 4);
244
245        // And the emitted hex should contain the CRC bytes (endianness applied)
246        let crc_location = super::validate_crc_location(4usize, &header).expect("crc loc");
247        assert_eq!(crc_location as usize, 4, "crc should follow payload end");
248        let crc_val = checksum::calculate_crc(&bytestream[..crc_location as usize], &settings.crc);
249        let crc_bytes = match settings.endianness {
250            Endianness::Big => crc_val.to_be_bytes(),
251            Endianness::Little => crc_val.to_le_bytes(),
252        };
253        // No byte swap in this test
254        let expected_crc_ascii = crc_bytes
255            .iter()
256            .map(|b| format!("{:02X}", b))
257            .collect::<String>();
258        assert!(
259            hex.to_uppercase().contains(&expected_crc_ascii),
260            "hex should contain CRC bytes"
261        );
262    }
263    #[test]
264    fn pad_to_end_true_resizes_to_full_block() {
265        let settings = sample_settings();
266        let header = sample_header(32);
267
268        let bytestream = vec![1u8, 2, 3, 4];
269        let dr = bytestream_to_datarange(bytestream, &header, &settings, false, true, 0)
270            .expect("data range generation failed");
271
272        assert_eq!(dr.bytestream.len(), header.length as usize);
273    }
274}