bin_file 0.1.4

Mangling of various file formats that conveys binary information (Motorola S-Record, Intel HEX, TI-TXT and binary files).
Documentation
use std::{
    fmt::{Display, Write},
    str::FromStr,
};

use ansic::ansi;

use crate::{checksums::crc_ext_tek_hex, records::Record, Error};

const R: &str = ansi!(reset);
const GREEN: &str = ansi!(green);
const BLUE: &str = ansi!(blue);
const RED: &str = ansi!(red);
const CYAN: &str = ansi!(cyan);
const YELLOW: &str = ansi!(yellow);
const BRIGHT_CYAN: &str = ansi!(br.cyan); // 96
const BRIGHT_MAGENTA: &str = ansi!(br.magenta); // 95

/// Record of a Extended Tektronix Object Format
#[derive(PartialEq, Eq, Debug, Clone, Hash)]
pub enum ExtTekHexRecord {
    /// Data Record Type (6)
    Data {
        /// Load Address
        address: usize,
        /// Object code
        value: Vec<u8>,
    },
    /// Termination Recrod Type (8)
    Termination {
        /// Start Address
        start_address: usize,
    },
    /// Symbol Record (3)
    Symbol(String),
}

impl Record for ExtTekHexRecord {
    fn to_record_string(&self) -> Result<String, Error> {
        match self {
            ExtTekHexRecord::Data { address, value } => {
                let address_string = format!("{:X}", address);
                let block_length = value.len() * 2 + 6 + address_string.len();
                // The result string is twice as long as the value.
                let result_length = 2 * value.len();
                let data_str = String::with_capacity(result_length);

                // Construct the record.
                let data_str = value.iter().try_fold(data_str, |mut acc, byte| {
                    write!(&mut acc, "{:02X}", byte)
                        .map_err(|_| Error::SynthesisFailed)
                        .map(|_| acc)
                })?;
                let checksum_str = format!(
                    "{:2X}6{:1X}{}{}",
                    block_length,
                    address_string.len(),
                    address_string,
                    &data_str
                );
                Ok(format!(
                    "%{:2X}6{:2X}{:1X}{}{}",
                    block_length,
                    crc_ext_tek_hex(checksum_str.chars().collect::<Vec<char>>()),
                    address_string.len(),
                    address_string,
                    &data_str
                ))
            }
            ExtTekHexRecord::Termination { start_address } => {
                let address_string = format!("{:X}", start_address);
                let block_length = 6 + address_string.len();
                let checksum_str = format!(
                    "{:2X}8{:1X}{}",
                    block_length,
                    address_string.len(),
                    address_string,
                );
                Ok(format!(
                    "%{:2X}8{:2X}{:1X}{}",
                    block_length,
                    crc_ext_tek_hex(checksum_str.chars().collect::<Vec<char>>()),
                    address_string.len(),
                    address_string,
                ))
            }
            ExtTekHexRecord::Symbol(value) => {
                let block_length = value.len() + 6;
                let checksum_str = format!("{:2X}3{}", block_length, value);
                Ok(format!(
                    "%{:2X}3{:2X}{}",
                    block_length,
                    crc_ext_tek_hex(checksum_str.chars().collect::<Vec<char>>()),
                    value
                ))
            }
        }
    }

    fn to_pretty_record_string(&self) -> Result<String, Error> {
        let record_string = self.to_record_string()?;
        let (type_str, type_txt) = match self {
            ExtTekHexRecord::Data { .. } => {
                (format!("{GREEN}{}{R}", &record_string[3..4]), " (data)")
            }
            ExtTekHexRecord::Termination { .. } => (
                format!("{BRIGHT_CYAN}{}{R}", &record_string[3..4]),
                " (termination)",
            ),
            ExtTekHexRecord::Symbol(_) => {
                (format!("{BLUE}{}{R}", &record_string[3..4]), " (symbol)")
            }
        };
        Ok(format!(
            "{RED}{}{R}{BRIGHT_MAGENTA}{}{R}{}{CYAN}{}{R}{YELLOW}{}{R}{}",
            &record_string[..1],
            &record_string[1..3],
            type_str,
            &record_string[4..6],
            &record_string[7..],
            type_txt
        ))
    }

    fn from_record_string<S>(record_string: S) -> Result<Self, Error>
    where
        S: AsRef<str>,
    {
        let string = record_string.as_ref().trim();
        if let Some('%') = string.chars().next() {
            if let Ok(length) = u8::from_str_radix(&string[1..3], 16) {
                match (string.len() - 1).cmp(&(length as usize)) {
                    std::cmp::Ordering::Less => Err(Error::RecordTooLong),
                    std::cmp::Ordering::Equal => {
                        let checksum_str = string[1..4].to_string() + &string[6..];
                        let checksum = crc_ext_tek_hex(checksum_str.chars().collect::<Vec<char>>());
                        if let Ok(expected_checksum) = u8::from_str_radix(&string[4..6], 16) {
                            if expected_checksum != checksum {
                                Err(Error::ChecksumMismatch(expected_checksum, checksum))
                            } else if let Ok(block_type) = u8::from_str_radix(&string[3..4], 16) {
                                if let Ok(address_length) = u8::from_str_radix(&string[6..7], 16) {
                                    if let Ok(address) = usize::from_str_radix(
                                        &string[7..7 + address_length as usize],
                                        16,
                                    ) {
                                        match block_type {
                                            // Symbol Record
                                            3 => {
                                                let data_str = &string[7..];
                                                Ok(ExtTekHexRecord::Symbol(data_str.to_owned()))
                                            }
                                            // Data Record
                                            6 => {
                                                let data_str =
                                                    &string[7 + address_length as usize..];
                                                // Convert the character stream to bytes.
                                                let data_bytes = data_str
                                                    .as_bytes()
                                                    .chunks(2)
                                                    .map(|chunk| {
                                                        std::str::from_utf8(chunk).unwrap()
                                                    })
                                                    .map(|byte_str| {
                                                        u8::from_str_radix(byte_str, 16).unwrap()
                                                    })
                                                    .collect::<Vec<u8>>();
                                                Ok(ExtTekHexRecord::Data {
                                                    address,
                                                    value: data_bytes,
                                                })
                                            }
                                            8 => Ok(ExtTekHexRecord::Termination {
                                                start_address: address,
                                            }),
                                            _ => Err(Error::UnsupportedRecordType {
                                                record: string.to_string(),
                                                record_type: block_type,
                                            }),
                                        }
                                    } else {
                                        Err(Error::InvalidAddressRange)
                                    }
                                } else {
                                    Err(Error::InvalidAddressRange)
                                }
                            } else {
                                Err(Error::UnsupportedRecordType {
                                    record: string.to_string(),
                                    record_type: 0xFF,
                                })
                            }
                        } else {
                            Err(Error::ChecksumMismatch(checksum, 0xff))
                        }
                    }
                    std::cmp::Ordering::Greater => Err(Error::RecordTooShort),
                }
            } else {
                Err(Error::InvalidLengthForType)
            }
        } else {
            Err(Error::MissingStartCode)
        }
    }

    fn is_record_str_correct<S>(record_str: S) -> bool
    where
        S: AsRef<str>,
    {
        Self::from_record_string(record_str).is_ok()
    }
}

impl FromStr for ExtTekHexRecord {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::from_record_string(s)
    }
}

impl Display for ExtTekHexRecord {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ExtTekHexRecord::Data { address, value } => {
                write!(f, "Data Record: Address: {}, Value: {:?}", address, value)
            }
            ExtTekHexRecord::Termination { start_address } => {
                write!(f, "Termination Record: Start address: {}", start_address)
            }
            ExtTekHexRecord::Symbol(symbol) => write!(f, "Symbol Record: {}", symbol),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_io_record() {
        let record = ExtTekHexRecord::from_record_string("%1A626810000000202020202020");
        assert_eq!(
            record,
            Ok(ExtTekHexRecord::Data {
                address: 0x10000000,
                value: vec![0x20; 6]
            })
        );
    }

    #[test]
    fn test_io_record_3() {
        let record = ExtTekHexRecord::from_record_string("%1561C3100202020202020");
        assert_eq!(
            record,
            Ok(ExtTekHexRecord::Data {
                address: 0x100,
                value: vec![0x20; 6]
            })
        );
    }

    #[test]
    fn test_to_record_string() {
        assert_eq!(
            ExtTekHexRecord::Data {
                address: 0x10000000,
                value: vec![0x20; 6]
            }
            .to_record_string(),
            Ok("%1A626810000000202020202020".into())
        );
        assert_eq!(
            ExtTekHexRecord::Data {
                address: 0x100,
                value: vec![0x20; 6]
            }
            .to_record_string(),
            Ok("%1561C3100202020202020".into())
        );
    }
}