sftool_lib/
utils.rs

1use crate::WriteFlashFile;
2use crc::Algorithm;
3use memmap2::Mmap;
4use std::fs::File;
5use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
6use std::num::ParseIntError;
7use std::path::Path;
8use tempfile::tempfile;
9
10#[derive(Debug, PartialEq, Eq, Clone)]
11pub enum FileType {
12    Bin,
13    Hex,
14    Elf,
15}
16
17pub const ELF_MAGIC: &[u8] = &[0x7F, 0x45, 0x4C, 0x46]; // ELF file magic number
18
19pub struct Utils;
20impl Utils {
21    pub fn str_to_u32(s: &str) -> Result<u32, ParseIntError> {
22        let s = s.trim();
23
24        let (num_str, multiplier) = match s.chars().last() {
25            Some('k') | Some('K') => (&s[..s.len() - 1], 1_000u32),
26            Some('m') | Some('M') => (&s[..s.len() - 1], 1_000_000u32),
27            Some('g') | Some('G') => (&s[..s.len() - 1], 1_000_000_000u32),
28            _ => (s, 1),
29        };
30
31        let unsigned: u32 = if let Some(hex) = num_str.strip_prefix("0x") {
32            u32::from_str_radix(hex, 16)?
33        } else if let Some(bin) = num_str.strip_prefix("0b") {
34            u32::from_str_radix(bin, 2)?
35        } else if let Some(oct) = num_str.strip_prefix("0o") {
36            u32::from_str_radix(oct, 8)?
37        } else {
38            num_str.parse()?
39        };
40
41        Ok(unsigned * multiplier)
42    }
43
44    pub(crate) fn get_file_crc32(file: &File) -> Result<u32, std::io::Error> {
45        const CRC_32_ALGO: Algorithm<u32> = Algorithm {
46            width: 32,
47            poly: 0x04C11DB7,
48            init: 0,
49            refin: true,
50            refout: true,
51            xorout: 0,
52            check: 0x2DFD2D88,
53            residue: 0,
54        };
55
56        const CRC: crc::Crc<u32> = crc::Crc::<u32>::new(&CRC_32_ALGO);
57        let mut reader = BufReader::new(file);
58
59        let mut digest = CRC.digest();
60
61        let mut buffer = [0u8; 4 * 1024];
62        loop {
63            let n = reader.read(&mut buffer)?;
64            if n == 0 {
65                break;
66            }
67            digest.update(&buffer[..n]);
68        }
69
70        let checksum = digest.finalize();
71        reader.seek(SeekFrom::Start(0))?;
72        Ok(checksum)
73    }
74
75    /// 文件类型检测
76    pub fn detect_file_type(path: &Path) -> Result<FileType, std::io::Error> {
77        if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
78            match ext.to_lowercase().as_str() {
79                "bin" => return Ok(FileType::Bin),
80                "hex" => return Ok(FileType::Hex),
81                "elf" | "axf" => return Ok(FileType::Elf),
82                _ => {} // 如果扩展名无法识别,继续检查MAGIC
83            }
84        }
85
86        // 如果没有可识别的扩展名,则检查文件MAGIC
87        let mut file = File::open(path)?;
88        let mut magic = [0u8; 4];
89        file.read_exact(&mut magic)?;
90
91        if magic == ELF_MAGIC {
92            return Ok(FileType::Elf);
93        }
94
95        Err(std::io::Error::new(
96            std::io::ErrorKind::InvalidInput,
97            "Unrecognized file type",
98        ))
99    }
100
101    /// 解析文件信息,支持file@address格式
102    pub fn parse_file_info(file_str: &str) -> Result<Vec<WriteFlashFile>, std::io::Error> {
103        // file@address
104        let parts: Vec<_> = file_str.split('@').collect();
105        // 如果存在@符号,则证明是bin文件
106        if parts.len() == 2 {
107            let addr = Self::str_to_u32(parts[1])
108                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
109            let file = std::fs::File::open(parts[0])?;
110            let crc32 = Self::get_file_crc32(&file)?;
111
112            return Ok(vec![WriteFlashFile {
113                address: addr,
114                file,
115                crc32,
116            }]);
117        }
118
119        let file_type = Self::detect_file_type(Path::new(parts[0]))?;
120
121        match file_type {
122            FileType::Hex => Self::hex_to_write_flash_files(Path::new(parts[0])),
123            FileType::Elf => Self::elf_to_write_flash_files(Path::new(parts[0])),
124            FileType::Bin => Err(std::io::Error::new(
125                std::io::ErrorKind::InvalidInput,
126                "For binary files, please use the <file@address> format",
127            )),
128        }
129    }
130
131    /// 计算数据的CRC32
132    pub fn calculate_crc32(data: &[u8]) -> u32 {
133        const CRC_32_ALGO: Algorithm<u32> = Algorithm {
134            width: 32,
135            poly: 0x04C11DB7,
136            init: 0,
137            refin: true,
138            refout: true,
139            xorout: 0,
140            check: 0,
141            residue: 0,
142        };
143        crc::Crc::<u32>::new(&CRC_32_ALGO).checksum(data)
144    }
145
146    /// 将HEX文件转换为WriteFlashFile
147    pub fn hex_to_write_flash_files(
148        hex_file: &Path,
149    ) -> Result<Vec<WriteFlashFile>, std::io::Error> {
150        let mut write_flash_files: Vec<WriteFlashFile> = Vec::new();
151
152        let file = std::fs::File::open(hex_file)?;
153        let reader = std::io::BufReader::new(file);
154
155        let mut current_base_address = 0u32;
156        let mut current_temp_file: Option<File> = None;
157        let mut current_segment_start = 0u32;
158        let mut current_file_offset = 0u32;
159
160        for line in reader.lines() {
161            let line = line?;
162            let line = line.trim_end_matches('\r');
163            if line.is_empty() {
164                continue;
165            }
166
167            let ihex_record = ihex::Record::from_record_string(&line)
168                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
169
170            match ihex_record {
171                ihex::Record::ExtendedLinearAddress(addr) => {
172                    let new_base_address = (addr as u32) << 16;
173
174                    // If base address changes, finalize current segment and start a new one
175                    if new_base_address != current_base_address && current_temp_file.is_some() {
176                        // Finalize current segment
177                        if let Some(temp_file) = current_temp_file.take() {
178                            Self::finalize_segment(
179                                temp_file,
180                                current_segment_start,
181                                &mut write_flash_files,
182                            )?;
183                        }
184                        current_file_offset = 0;
185                    }
186
187                    current_base_address = new_base_address;
188                }
189                ihex::Record::Data { offset, value } => {
190                    let absolute_address = current_base_address + offset as u32;
191
192                    // If this is the first data record or start of a new segment
193                    if current_temp_file.is_none() {
194                        current_temp_file = Some(tempfile()?);
195                        current_segment_start = absolute_address;
196                        current_file_offset = 0;
197                    }
198
199                    if let Some(ref mut temp_file) = current_temp_file {
200                        let expected_file_offset = absolute_address - current_segment_start;
201
202                        // Fill gaps with 0xFF if they exist
203                        if expected_file_offset > current_file_offset {
204                            let gap_size = expected_file_offset - current_file_offset;
205                            let fill_data = vec![0xFF; gap_size as usize];
206                            temp_file.write_all(&fill_data)?;
207                            current_file_offset = expected_file_offset;
208                        }
209
210                        // Write data
211                        temp_file.write_all(&value)?;
212                        current_file_offset += value.len() as u32;
213                    }
214                }
215                ihex::Record::EndOfFile => {
216                    // Finalize the last segment
217                    if let Some(temp_file) = current_temp_file.take() {
218                        Self::finalize_segment(
219                            temp_file,
220                            current_segment_start,
221                            &mut write_flash_files,
222                        )?;
223                    }
224                    break;
225                }
226                _ => {}
227            }
228        }
229
230        // If file ends without encountering EndOfFile record, finalize current segment
231        if let Some(temp_file) = current_temp_file.take() {
232            Self::finalize_segment(temp_file, current_segment_start, &mut write_flash_files)?;
233        }
234
235        Ok(write_flash_files)
236    }
237
238    /// 将ELF文件转换为WriteFlashFile  
239    pub fn elf_to_write_flash_files(
240        elf_file: &Path,
241    ) -> Result<Vec<WriteFlashFile>, std::io::Error> {
242        let mut write_flash_files: Vec<WriteFlashFile> = Vec::new();
243        const SECTOR_SIZE: u32 = 0x1000; // 扇区大小
244        const FILL_BYTE: u8 = 0xFF; // 填充字节
245
246        let file = File::open(elf_file)?;
247        let mmap = unsafe { Mmap::map(&file)? };
248        let elf = goblin::elf::Elf::parse(&mmap[..])
249            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
250
251        // 收集所有需要烧录的段
252        let mut load_segments: Vec<_> = elf
253            .program_headers
254            .iter()
255            .filter(|ph| {
256                ph.p_type == goblin::elf::program_header::PT_LOAD && ph.p_paddr < 0x2000_0000
257            })
258            .collect();
259        load_segments.sort_by_key(|ph| ph.p_paddr);
260
261        if load_segments.is_empty() {
262            return Ok(write_flash_files);
263        }
264
265        let mut current_file = tempfile()?;
266        let mut current_base = (load_segments[0].p_paddr as u32) & !(SECTOR_SIZE - 1);
267        let mut current_offset = 0; // 跟踪当前文件中的偏移量
268
269        for ph in load_segments.iter() {
270            let vaddr = ph.p_paddr as u32;
271            let offset = ph.p_offset as usize;
272            let size = ph.p_filesz as usize;
273            let data = &mmap[offset..offset + size];
274
275            // 计算当前段的对齐基地址
276            let segment_base = vaddr & !(SECTOR_SIZE - 1);
277
278            // 如果超出了当前对齐块,创建新文件
279            if segment_base > current_base + current_offset {
280                current_file.seek(std::io::SeekFrom::Start(0))?;
281                let crc32 = Self::get_file_crc32(&current_file)?;
282                write_flash_files.push(WriteFlashFile {
283                    address: current_base,
284                    file: std::mem::replace(&mut current_file, tempfile()?),
285                    crc32,
286                });
287                current_base = segment_base;
288                current_offset = 0;
289            }
290
291            // 计算相对于当前文件基地址的偏移
292            let relative_offset = vaddr - current_base;
293
294            // 如果当前偏移小于目标偏移,填充间隙
295            if current_offset < relative_offset {
296                let padding = relative_offset - current_offset;
297                current_file.write_all(&vec![FILL_BYTE; padding as usize])?;
298                current_offset = relative_offset;
299            }
300
301            // 写入数据
302            current_file.write_all(data)?;
303            current_offset += size as u32;
304        }
305
306        // 处理最后一个文件
307        if current_offset > 0 {
308            current_file.seek(std::io::SeekFrom::Start(0))?;
309            let crc32 = Self::get_file_crc32(&current_file)?;
310            write_flash_files.push(WriteFlashFile {
311                address: current_base,
312                file: current_file,
313                crc32,
314            });
315        }
316
317        Ok(write_flash_files)
318    }
319
320    /// 完成一个段的处理,将临时文件转换为WriteFlashFile
321    fn finalize_segment(
322        mut temp_file: File,
323        address: u32,
324        write_flash_files: &mut Vec<WriteFlashFile>,
325    ) -> Result<(), std::io::Error> {
326        temp_file.seek(std::io::SeekFrom::Start(0))?;
327        let crc32 = Self::get_file_crc32(&temp_file)?;
328        write_flash_files.push(WriteFlashFile {
329            address,
330            file: temp_file,
331            crc32,
332        });
333        Ok(())
334    }
335
336    /// 解析读取文件信息 (filename@address:size格式)
337    pub fn parse_read_file_info(file_spec: &str) -> Result<crate::ReadFlashFile, std::io::Error> {
338        let Some((file_path, addr_size)) = file_spec.split_once('@') else {
339            return Err(std::io::Error::new(
340                std::io::ErrorKind::InvalidInput,
341                format!(
342                    "Invalid format: {}. Expected: filename@address:size",
343                    file_spec
344                ),
345            ));
346        };
347
348        let Some((address_str, size_str)) = addr_size.split_once(':') else {
349            return Err(std::io::Error::new(
350                std::io::ErrorKind::InvalidInput,
351                format!(
352                    "Invalid address:size format: {}. Expected: address:size",
353                    addr_size
354                ),
355            ));
356        };
357
358        let address = Self::str_to_u32(address_str).map_err(|e| {
359            std::io::Error::new(
360                std::io::ErrorKind::InvalidInput,
361                format!("Invalid address '{}': {}", address_str, e),
362            )
363        })?;
364
365        let size = Self::str_to_u32(size_str).map_err(|e| {
366            std::io::Error::new(
367                std::io::ErrorKind::InvalidInput,
368                format!("Invalid size '{}': {}", size_str, e),
369            )
370        })?;
371
372        Ok(crate::ReadFlashFile {
373            file_path: file_path.to_string(),
374            address,
375            size,
376        })
377    }
378
379    /// 解析擦除地址
380    pub fn parse_erase_address(address_str: &str) -> Result<u32, std::io::Error> {
381        Self::str_to_u32(address_str).map_err(|e| {
382            std::io::Error::new(
383                std::io::ErrorKind::InvalidInput,
384                format!("Invalid address '{}': {}", address_str, e),
385            )
386        })
387    }
388
389    /// 解析擦除区域信息 (address:size格式)
390    pub fn parse_erase_region(region_spec: &str) -> Result<crate::EraseRegionFile, std::io::Error> {
391        let Some((address_str, size_str)) = region_spec.split_once(':') else {
392            return Err(std::io::Error::new(
393                std::io::ErrorKind::InvalidInput,
394                format!(
395                    "Invalid region format: {}. Expected: address:size",
396                    region_spec
397                ),
398            ));
399        };
400
401        let address = Self::str_to_u32(address_str).map_err(|e| {
402            std::io::Error::new(
403                std::io::ErrorKind::InvalidInput,
404                format!("Invalid address '{}': {}", address_str, e),
405            )
406        })?;
407
408        let size = Self::str_to_u32(size_str).map_err(|e| {
409            std::io::Error::new(
410                std::io::ErrorKind::InvalidInput,
411                format!("Invalid size '{}': {}", size_str, e),
412            )
413        })?;
414
415        Ok(crate::EraseRegionFile { address, size })
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use std::io::{Read, Seek, SeekFrom, Write};
423    use tempfile::NamedTempFile;
424
425    #[test]
426    fn test_hex_to_bin_single_segment() {
427        // Create a simple hex file with one segment using correct Intel HEX checksums
428        let hex_content = ":0400000001020304F2\n:0410000005060708D2\n:00000001FF\n";
429
430        let mut temp_hex = NamedTempFile::new().unwrap();
431        temp_hex.write_all(hex_content.as_bytes()).unwrap();
432
433        let result = Utils::hex_to_write_flash_files(temp_hex.path()).unwrap();
434
435        // Should have one segment
436        assert_eq!(result.len(), 1);
437
438        let segment = &result[0];
439        assert_eq!(segment.address, 0x00000000);
440
441        // Check data size (gap filled from 0x0000 to 0x1003)
442        let file_size = segment.file.metadata().unwrap().len() as usize;
443        assert_eq!(file_size, 0x1004);
444
445        // Read file content to verify data
446        let mut file_data = Vec::new();
447        let mut file = &segment.file;
448        file.read_to_end(&mut file_data).unwrap();
449
450        // Verify gap filling
451        // First 4 bytes should be the original data: 01 02 03 04
452        assert_eq!(&file_data[0..4], &[0x01, 0x02, 0x03, 0x04]);
453        // Gap between 0x04 and 0x1000 should be filled with 0xFF
454        assert!(file_data[4..0x1000].iter().all(|&b| b == 0xFF));
455        // Last 4 bytes should be: 05 06 07 08
456        assert_eq!(&file_data[0x1000..0x1004], &[0x05, 0x06, 0x07, 0x08]);
457    }
458
459    #[test]
460    fn test_hex_to_bin_multiple_segments() {
461        // Create a hex file with multiple segments using correct checksums
462        let hex_content =
463            ":0400000001020304F2\n:020000040001F9\n:0400000011121314B2\n:00000001FF\n";
464
465        let mut temp_hex = NamedTempFile::new().unwrap();
466        temp_hex.write_all(hex_content.as_bytes()).unwrap();
467
468        let result = Utils::hex_to_write_flash_files(temp_hex.path()).unwrap();
469
470        // Should have two segments
471        assert_eq!(result.len(), 2);
472
473        // First segment at 0x00000000
474        assert_eq!(result[0].address, 0x00000000);
475        let file_size_0 = result[0].file.metadata().unwrap().len() as usize;
476        assert_eq!(file_size_0, 4);
477
478        let mut file_data_0 = Vec::new();
479        let mut file_0 = &result[0].file;
480        file_0.read_to_end(&mut file_data_0).unwrap();
481        assert_eq!(&file_data_0, &[0x01, 0x02, 0x03, 0x04]);
482
483        // Second segment at 0x00010000
484        assert_eq!(result[1].address, 0x00010000);
485        let file_size_1 = result[1].file.metadata().unwrap().len() as usize;
486        assert_eq!(file_size_1, 4);
487
488        let mut file_data_1 = Vec::new();
489        let mut file_1 = &result[1].file;
490        file_1.read_to_end(&mut file_data_1).unwrap();
491        assert_eq!(&file_data_1, &[0x11, 0x12, 0x13, 0x14]);
492    }
493
494    #[test]
495    fn test_hex_to_bin_with_gaps() {
496        // Create a hex file with gaps that should be filled with 0xFF
497        let hex_content = ":04000000AABBCCDDEE\n:04100000EEFF0011EE\n:00000001FF\n";
498
499        let mut temp_hex = NamedTempFile::new().unwrap();
500        temp_hex.write_all(hex_content.as_bytes()).unwrap();
501
502        let result = Utils::hex_to_write_flash_files(temp_hex.path()).unwrap();
503
504        // Debug: print actual results
505        println!("Number of segments: {}", result.len());
506        for (i, segment) in result.iter().enumerate() {
507            let file_size = segment.file.metadata().unwrap().len() as usize;
508            println!(
509                "Segment {}: address=0x{:08X}, size={}",
510                i, segment.address, file_size
511            );
512        }
513
514        // Should have one segment
515        assert_eq!(result.len(), 1);
516
517        let segment = &result[0];
518        assert_eq!(segment.address, 0x00000000);
519
520        // Should have 4 bytes data + 4092 bytes gap + 4 bytes data = 4100 bytes
521        let file_size = segment.file.metadata().unwrap().len() as usize;
522        println!(
523            "Expected size: 0x1004 ({}), Actual size: {}",
524            0x1004, file_size
525        );
526        assert_eq!(file_size, 0x1004);
527
528        // Read file content to verify data
529        let mut file_data = Vec::new();
530        let mut file = &segment.file;
531        file.read_to_end(&mut file_data).unwrap();
532
533        // Verify first 4 bytes
534        assert_eq!(&file_data[0..4], &[0xAA, 0xBB, 0xCC, 0xDD]);
535        // Verify gap is filled with 0xFF
536        assert!(file_data[4..0x1000].iter().all(|&b| b == 0xFF));
537        // Verify last 4 bytes
538        assert_eq!(&file_data[0x1000..0x1004], &[0xEE, 0xFF, 0x00, 0x11]);
539
540        // Read the file and check gap is filled with 0xFF
541        let mut file = segment.file.try_clone().unwrap();
542        file.seek(SeekFrom::Start(4)).unwrap();
543        let mut gap_data = vec![0; 0x1000 - 4];
544        file.read_exact(&mut gap_data).unwrap();
545
546        // All gap bytes should be 0xFF
547        assert!(gap_data.iter().all(|&b| b == 0xFF));
548    }
549
550    #[test]
551    fn test_hex_to_bin_complex_multi_segment() {
552        // Create a complex hex file with multiple segments, gaps, and different sizes
553        let hex_content = ":100000000102030405060708090A0B0C0D0E0F1068\n:08100000111213141516171844\n:020000040001F9\n:040000002122232472\n:041000003132333422\n:020000040010EA\n:080000004142434445464748D4\n:00000001FF\n";
554
555        let mut temp_hex = NamedTempFile::new().unwrap();
556        temp_hex.write_all(hex_content.as_bytes()).unwrap();
557
558        let result = Utils::hex_to_write_flash_files(temp_hex.path()).unwrap();
559
560        // Should have three segments
561        assert_eq!(result.len(), 3);
562
563        // First segment at 0x00000000 (contains data at 0x0000 and 0x1000 with gap)
564        assert_eq!(result[0].address, 0x00000000);
565        let file_size_0 = result[0].file.metadata().unwrap().len() as usize;
566        assert_eq!(file_size_0, 0x1008); // 0x1000 + 8 bytes
567
568        // Second segment at 0x00010000 (contains data at 0x0000 and 0x1000 with gap)
569        assert_eq!(result[1].address, 0x00010000);
570        let file_size_1 = result[1].file.metadata().unwrap().len() as usize;
571        assert_eq!(file_size_1, 0x1004); // 0x1000 + 4 bytes
572
573        // Third segment at 0x00100000
574        assert_eq!(result[2].address, 0x00100000);
575        let file_size_2 = result[2].file.metadata().unwrap().len() as usize;
576        assert_eq!(file_size_2, 8);
577
578        // Read file content to verify data for first segment
579        let mut file_data_0 = Vec::new();
580        let mut file_0 = &result[0].file;
581        file_0.read_to_end(&mut file_data_0).unwrap();
582
583        // Verify gap filling in first segment
584        // First 16 bytes should be the original data: 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10
585        assert_eq!(
586            &file_data_0[0..16],
587            &[
588                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
589                0x0F, 0x10
590            ]
591        );
592        // Gap between 0x10 and 0x1000 should be filled with 0xFF
593        assert!(file_data_0[16..0x1000].iter().all(|&b| b == 0xFF));
594        // Last 8 bytes should be the second data block
595        assert_eq!(
596            &file_data_0[0x1000..0x1008],
597            &[0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18]
598        );
599    }
600
601    #[test]
602    fn test_str_to_u32() {
603        assert_eq!(Utils::str_to_u32("123").unwrap(), 123);
604        assert_eq!(Utils::str_to_u32("0x10").unwrap(), 16);
605        assert_eq!(Utils::str_to_u32("0b1010").unwrap(), 10);
606        assert_eq!(Utils::str_to_u32("0o17").unwrap(), 15);
607        assert_eq!(Utils::str_to_u32("1k").unwrap(), 1000);
608        assert_eq!(Utils::str_to_u32("1K").unwrap(), 1000);
609        assert_eq!(Utils::str_to_u32("1m").unwrap(), 1000000);
610        assert_eq!(Utils::str_to_u32("1M").unwrap(), 1000000);
611    }
612
613    #[test]
614    fn test_parse_read_file_info() {
615        let result = Utils::parse_read_file_info("output.bin@0x1000:0x100").unwrap();
616        assert_eq!(result.file_path, "output.bin");
617        assert_eq!(result.address, 0x1000);
618        assert_eq!(result.size, 0x100);
619
620        let result = Utils::parse_read_file_info("data.bin@0x20000000:1k").unwrap();
621        assert_eq!(result.file_path, "data.bin");
622        assert_eq!(result.address, 0x20000000);
623        assert_eq!(result.size, 1000);
624
625        // Test error cases
626        assert!(Utils::parse_read_file_info("invalid_format").is_err());
627        assert!(Utils::parse_read_file_info("file@0x1000").is_err()); // missing size
628        assert!(Utils::parse_read_file_info("file@invalid:0x100").is_err()); // invalid address
629    }
630
631    #[test]
632    fn test_parse_erase_address() {
633        assert_eq!(Utils::parse_erase_address("0x1000").unwrap(), 0x1000);
634        assert_eq!(Utils::parse_erase_address("1000").unwrap(), 1000);
635        assert_eq!(Utils::parse_erase_address("1k").unwrap(), 1000);
636
637        // Test error cases
638        assert!(Utils::parse_erase_address("invalid").is_err());
639    }
640
641    #[test]
642    fn test_parse_erase_region() {
643        let result = Utils::parse_erase_region("0x1000:0x100").unwrap();
644        assert_eq!(result.address, 0x1000);
645        assert_eq!(result.size, 0x100);
646
647        let result = Utils::parse_erase_region("0x20000000:1k").unwrap();
648        assert_eq!(result.address, 0x20000000);
649        assert_eq!(result.size, 1000);
650
651        // Test error cases
652        assert!(Utils::parse_erase_region("invalid_format").is_err());
653        assert!(Utils::parse_erase_region("0x1000").is_err()); // missing size
654        assert!(Utils::parse_erase_region("invalid:0x100").is_err()); // invalid address
655    }
656}