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
13fn 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
30fn resolve_crc(
32 length: usize,
33 header: &Header,
34 settings: &Settings,
35 block_len_bytes: u32,
36) -> Result<Option<(u32, CrcConfig)>, OutputError> {
37 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 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 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 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 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 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 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(); let is_end_block = matches!(
168 &crc_settings.location,
169 Some(CrcLocation::Keyword(kw)) if kw == "end_block"
170 );
171
172 let crc_val = match area {
174 CrcArea::Data => {
175 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 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 bytestream.resize(block_len_bytes as usize, header.padding);
195 checksum::calculate_crc(&bytestream, &crc_settings)
196 }
197 CrcArea::BlockOmitCrc => {
198 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 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 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 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#[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 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 assert_eq!(bytestream.len(), 4);
382
383 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 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 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 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 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 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 let header = Header {
559 crc: Some(CrcConfig {
560 location: Some(CrcLocation::Keyword("end_data".to_string())),
561 polynomial: Some(0x1EDC6F41), ..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 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 let settings = Settings {
583 crc: None,
584 ..sample_settings()
585 };
586
587 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 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 let settings = Settings {
608 crc: Some(sample_crc_config()),
609 ..sample_settings()
610 };
611
612 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 assert!(!dr.crc_bytestream.is_empty());
621 }
622
623 #[test]
624 fn settings_absolute_address_rejected() {
625 let settings = Settings {
627 crc: Some(CrcConfig {
628 location: Some(CrcLocation::Address(0x1000)),
629 ..sample_crc_config()
630 }),
631 ..sample_settings()
632 };
633
634 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 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 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]; 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}