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 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 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 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 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 if let CrcLocation::Keyword(_) = &header.crc_location {
119 bytestream.resize(crc_offset as usize, header.padding);
120 }
121
122 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 if crc_settings.area == CrcArea::BlockZeroCrc {
132 bytestream[crc_offset as usize..(crc_offset + 4) as usize].fill(0);
133 }
134
135 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 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 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 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 assert_eq!(bytestream.len(), 4);
295
296 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 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 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 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 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); 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}