Skip to main content

libsurfer/translation/
mapping_translators.rs

1use camino::Utf8Path;
2use ecolor::Color32;
3use num::BigUint;
4use std::collections::HashMap;
5use std::fs;
6use std::sync::OnceLock;
7use surfer_translation_types::{
8    BasicTranslator, TranslationPreference, ValueKind, VariableValue, extend_string,
9    kind_for_binary_representation,
10};
11use thiserror::Error;
12
13use crate::{
14    translation::check_single_wordlength,
15    wave_container::{ScopeId, VarId, VariableMeta},
16};
17
18static KIND_COLOR_KEYWORDS: OnceLock<HashMap<&'static str, ValueKind>> = OnceLock::new();
19
20fn kind_color_keywords() -> &'static HashMap<&'static str, ValueKind> {
21    KIND_COLOR_KEYWORDS.get_or_init(|| {
22        let mut m = HashMap::new();
23        m.insert("normal", ValueKind::Normal);
24        m.insert("default", ValueKind::Normal);
25        m.insert("undef", ValueKind::Undef);
26        m.insert("highimp", ValueKind::HighImp);
27        m.insert("warn", ValueKind::Warn);
28        m.insert("dontcare", ValueKind::DontCare);
29        m.insert("weak", ValueKind::Weak);
30        m.insert("error", ValueKind::Error);
31        m.insert("black", ValueKind::Custom(Color32::BLACK));
32        m.insert("white", ValueKind::Custom(Color32::WHITE));
33        m.insert("red", ValueKind::Custom(Color32::RED));
34        m.insert("green", ValueKind::Custom(Color32::GREEN));
35        m.insert("blue", ValueKind::Custom(Color32::BLUE));
36        m.insert("yellow", ValueKind::Custom(Color32::YELLOW));
37        m.insert("cyan", ValueKind::Custom(Color32::CYAN));
38        m.insert("magenta", ValueKind::Custom(Color32::MAGENTA));
39        m.insert("gray", ValueKind::Custom(Color32::GRAY));
40        m.insert("grey", ValueKind::Custom(Color32::GRAY));
41        m.insert("light_gray", ValueKind::Custom(Color32::LIGHT_GRAY));
42        m.insert("light_grey", ValueKind::Custom(Color32::LIGHT_GRAY));
43        m.insert("dark_gray", ValueKind::Custom(Color32::DARK_GRAY));
44        m.insert("dark_grey", ValueKind::Custom(Color32::DARK_GRAY));
45        m.insert("brown", ValueKind::Custom(Color32::BROWN));
46        m.insert("dark_red", ValueKind::Custom(Color32::DARK_RED));
47        m.insert("light_red", ValueKind::Custom(Color32::LIGHT_RED));
48        m.insert("orange", ValueKind::Custom(Color32::ORANGE));
49        m.insert("light_yellow", ValueKind::Custom(Color32::LIGHT_YELLOW));
50        m.insert("khaki", ValueKind::Custom(Color32::KHAKI));
51        m.insert("dark_green", ValueKind::Custom(Color32::DARK_GREEN));
52        m.insert("light_green", ValueKind::Custom(Color32::LIGHT_GREEN));
53        m.insert("dark_blue", ValueKind::Custom(Color32::DARK_BLUE));
54        m.insert("light_blue", ValueKind::Custom(Color32::LIGHT_BLUE));
55        m.insert("purple", ValueKind::Custom(Color32::PURPLE));
56        m.insert("gold", ValueKind::Custom(Color32::GOLD));
57        m
58    })
59}
60
61#[derive(Debug, Clone, PartialEq)]
62struct MappingEntry {
63    label: String,
64    kind: ValueKind,
65}
66
67#[derive(Debug, Clone)]
68/// A mapping of variable values to their string entries.
69struct MappingTranslatorMap {
70    name: String,
71    bits: u32,
72    entries: HashMap<VariableValue, MappingEntry>,
73}
74
75pub struct MappingTranslator {
76    map: MappingTranslatorMap,
77}
78
79#[derive(Debug, Error)]
80pub enum MappingParseError {
81    #[error("IO error: {0}")]
82    Io(#[from] std::io::Error),
83
84    #[error("Invalid bits value: {0}")]
85    InvalidBitsValue(String),
86
87    #[error("Invalid hex number: {0}")]
88    InvalidHex(String),
89
90    #[error("Invalid octal number: {0}")]
91    InvalidOctal(String),
92
93    #[error("Unknown kind/color: {0}")]
94    UnknownKindColor(String),
95
96    #[error(
97        "Binary string '{value}' requires {required} bits, but only {specified} bits specified"
98    )]
99    BinaryTooWide {
100        value: String,
101        required: u32,
102        specified: u32,
103    },
104
105    #[error(
106        "String '{value}' has {value_len} characters, expected {expected} characters to match bit width"
107    )]
108    StringLengthMismatch {
109        value: String,
110        value_len: u32,
111        expected: u32,
112    },
113
114    #[error(
115        "String '{value}' contains invalid characters. Only 01zx-hlwu are allowed. Maybe you are missing a 0b prefix for a binary string?"
116    )]
117    InvalidStringCharacters { value: String },
118
119    #[error("Missing mapping")]
120    MissingMapping,
121
122    #[error("Empty line")]
123    EmptyLine,
124
125    #[error("Line {line}: {message}\n  Content: {content}")]
126    LineError {
127        line: usize,
128        content: String,
129        message: String,
130    },
131}
132
133impl MappingTranslator {
134    pub fn new_from_file<P: AsRef<Utf8Path>>(path: P) -> Result<Self, MappingParseError> {
135        Ok(MappingTranslator {
136            map: MappingTranslatorMap::new(path)?,
137        })
138    }
139
140    pub fn bits(&self) -> u32 {
141        self.map.bits
142    }
143}
144
145impl BasicTranslator<VarId, ScopeId> for MappingTranslator {
146    fn name(&self) -> String {
147        self.map.name.clone()
148    }
149
150    fn basic_translate(&self, num_bits: u32, value: &VariableValue) -> (String, ValueKind) {
151        // Extend the value if it's a string to match num_bits
152        let lookup_value = match value {
153            VariableValue::BigUint(_) => value.clone(),
154            VariableValue::String(s) => {
155                let extended = format!("{extra_bits}{s}", extra_bits = extend_string(s, num_bits));
156                VariableValue::String(extended)
157            }
158        };
159
160        if let Some(entry) = self.map.entries.get(&lookup_value) {
161            return (entry.label.clone(), entry.kind);
162        }
163
164        let var_string = key_display(&lookup_value, num_bits);
165
166        let val_kind = kind_for_binary_representation(&var_string);
167        (var_string, val_kind)
168    }
169
170    fn translates(&self, variable: &VariableMeta) -> eyre::Result<TranslationPreference> {
171        check_single_wordlength(variable.num_bits, self.map.bits)
172    }
173}
174
175impl MappingTranslatorMap {
176    pub fn new<P: AsRef<Utf8Path>>(file: P) -> Result<Self, MappingParseError> {
177        parse_file(file)
178    }
179}
180
181/// Parse a file and return a MappingTranslatorMap.
182fn parse_file<P: AsRef<Utf8Path>>(path: P) -> Result<MappingTranslatorMap, MappingParseError> {
183    let utf = path.as_ref();
184    let content = fs::read_to_string(utf.as_std_path())?;
185
186    // Extract filename (without extension) for default name
187    let default_name = utf.file_stem().map(|s| s.to_string());
188
189    parse_content_with_default_name(&content, default_name)
190}
191
192/// Parse content of file and return a MappingTranslatorMap, falling back to default_name if no name is found in the file.
193fn parse_content_with_default_name(
194    content: &str,
195    default_name: Option<String>,
196) -> Result<MappingTranslatorMap, MappingParseError> {
197    let mut name = None;
198    let mut bits = None;
199    let mut raw_entries = Vec::new();
200
201    // Process all lines and determine a temporary list of entries
202    for (line_num, line_str) in content.lines().enumerate() {
203        let processed = line_str.trim();
204
205        // Skip empty lines and comments (only lines starting with '#')
206        if processed.is_empty() || processed.starts_with('#') {
207            continue;
208        }
209
210        // Check for Name line if we haven't found one yet
211        if let Some(name_value) = parse_specifier(processed, "name") {
212            if name.is_some() {
213                return Err(MappingParseError::LineError {
214                    line: line_num + 1,
215                    content: line_str.to_string(),
216                    message: "Multiple Name specifiers found".to_string(),
217                });
218            }
219            name = Some(name_value);
220            continue;
221        }
222
223        // Check for Bits line if we haven't found one yet
224        if let Some(bits_value) = parse_specifier(processed, "bits") {
225            if bits.is_some() {
226                return Err(MappingParseError::LineError {
227                    line: line_num + 1,
228                    content: line_str.to_string(),
229                    message: "Multiple Bits specifiers found".to_string(),
230                });
231            }
232            let bits_num = bits_value
233                .parse::<u32>()
234                .map_err(|_| MappingParseError::InvalidBitsValue(bits_value.to_string()))?;
235            bits = Some(bits_num);
236            continue;
237        }
238
239        match parse_line(processed) {
240            Ok(entry) => raw_entries.push(entry),
241            Err(e) => {
242                return Err(MappingParseError::LineError {
243                    line: line_num + 1,
244                    content: line_str.to_string(),
245                    message: e.to_string(),
246                });
247            }
248        }
249    }
250
251    // Determine bit width if not provided
252    let bit_width = bits.unwrap_or_else(|| {
253        // Find the largest value length among entries
254        raw_entries.iter().map(|entry| entry.3).max().unwrap_or(0)
255    });
256
257    // Validate and normalize all entries, building HashMap
258    let mut entries = HashMap::new();
259    for (value, label, kind, value_len) in raw_entries {
260        let key = normalize_first_column(&value, value_len, bit_width)?;
261
262        if entries.contains_key(&key) {
263            tracing::warn!(
264                "Duplicate mapping key '{}' encountered; keeping first occurrence",
265                key_display(&key, bit_width)
266            );
267            continue;
268        }
269        entries.insert(key, MappingEntry { label, kind });
270    }
271
272    // Use default_name if no name was found in the file
273    let final_name = name
274        .or(default_name)
275        .unwrap_or_else(|| "Unknown mapping".to_string());
276
277    Ok(MappingTranslatorMap {
278        name: final_name,
279        bits: bit_width,
280        entries,
281    })
282}
283
284/// Normalize the first column value according to its type and specified bit width.
285///
286/// - Check that numeric values fit within the specified bit width.
287/// - Check that strings fit within the specified bit width and extend them if they are shorter.
288/// - Convert string values that are binary strings to numeric values.
289fn normalize_first_column(
290    value: &VariableValue,
291    value_len: u32,
292    bit_width: u32,
293) -> Result<VariableValue, MappingParseError> {
294    match value {
295        VariableValue::BigUint(v) => {
296            if value_len > bit_width {
297                return Err(MappingParseError::BinaryTooWide {
298                    value: format!("{v:0width$b}", width = value_len as usize),
299                    required: value_len,
300                    specified: bit_width,
301                });
302            }
303            Ok(VariableValue::BigUint(v.clone()))
304        }
305        VariableValue::String(s) => {
306            // Check if the first column contains only '0' and '1' (binary string)
307            if s.chars().all(|c| c == '0' || c == '1') {
308                if value_len > bit_width {
309                    return Err(MappingParseError::BinaryTooWide {
310                        value: s.to_string(),
311                        required: value_len,
312                        specified: bit_width,
313                    });
314                }
315
316                let value = BigUint::parse_bytes(s.as_bytes(), 2)
317                    .expect("binary string should parse as BigUint");
318                Ok(VariableValue::BigUint(value))
319            } else {
320                // Check if larger than specified bit width
321                if s.len() > bit_width as usize {
322                    return Err(MappingParseError::StringLengthMismatch {
323                        value: s.to_string(),
324                        value_len: s.len() as u32,
325                        expected: bit_width,
326                    });
327                }
328
329                let extended = format!(
330                    "{extra}{body}",
331                    extra = extend_string(s, bit_width),
332                    body = s
333                );
334                Ok(VariableValue::String(extended))
335            }
336        }
337    }
338}
339
340#[inline]
341fn key_display(key: &VariableValue, bit_width: u32) -> String {
342    match key {
343        VariableValue::String(s) => s.clone(),
344        VariableValue::BigUint(v) => format!("{v:0width$b}", width = bit_width as usize),
345    }
346}
347
348/// Parse a line and extract variable value, label, value kind, and length of the value.
349fn parse_line(line: &str) -> Result<(VariableValue, String, ValueKind, u32), MappingParseError> {
350    let mut chars = line.char_indices().peekable();
351    while let Some((_, ch)) = chars.peek() {
352        if ch.is_whitespace() {
353            chars.next();
354        } else {
355            break;
356        }
357    }
358
359    let start = if let Some((idx, _)) = chars.peek().copied() {
360        idx
361    } else {
362        return Err(MappingParseError::EmptyLine);
363    };
364
365    let mut first_end = line.len();
366    for (idx, ch) in chars {
367        if ch.is_whitespace() {
368            first_end = idx;
369            break;
370        }
371    }
372
373    let first_token = &line[start..first_end];
374    let remainder = line[first_end..].trim_start();
375
376    if first_token.is_empty() {
377        return Err(MappingParseError::EmptyLine);
378    }
379
380    let (first, kind, first_len) = parse_key_with_kind(first_token)?;
381
382    if remainder.is_empty() {
383        return Err(MappingParseError::MissingMapping);
384    }
385
386    Ok((first, remainder.to_string(), kind, first_len))
387}
388
389/// Parse the value part of the first item into a [`VariableValue`]` and its bit width.
390///
391/// For binary strings and nine-value strings, the bit width is determined by the string length.
392/// For hex, octal, and decimal numbers, the bit width is determined by the number of bits required to represent the actual numeric value.
393fn parse_key_value(token: &str) -> Result<(VariableValue, u32), MappingParseError> {
394    // Support underscore separators
395    let cleaned = token.replace('_', "");
396
397    // Binary prefix (0b or 0B)
398    if cleaned.starts_with("0b") || cleaned.starts_with("0B") {
399        let bin_str = &cleaned[2..];
400        if let Some(val) = BigUint::parse_bytes(bin_str.as_bytes(), 2) {
401            // Preserve the binary string length (e.g., 0b0101 => 4 bits, not 3)
402            let bits = bin_str.len() as u32;
403            return Ok((VariableValue::BigUint(val), bits));
404        } else {
405            // String literal
406            let lower = bin_str.to_lowercase();
407
408            // Validate that string contains only valid characters: 01zx-hlwu
409            if lower
410                .chars()
411                .all(|c| matches!(c, '0' | '1' | 'z' | 'x' | '-' | 'h' | 'l' | 'w' | 'u'))
412            {
413                return Ok((VariableValue::String(lower.to_string()), lower.len() as u32));
414            }
415        }
416        return Err(MappingParseError::InvalidBitsValue(token.to_string()));
417    }
418
419    // Octal prefix (0o or 0O)
420    if cleaned.starts_with("0o") || cleaned.starts_with("0O") {
421        let oct_str = &cleaned[2..];
422        let num = BigUint::parse_bytes(oct_str.as_bytes(), 8)
423            .ok_or_else(|| MappingParseError::InvalidOctal(token.to_string()))?;
424        // Actual number of bits required to represent the octal number
425        // We do not want a multiple of 3 here, but the actual bit width
426        let bits = num.bits() as u32;
427        return Ok((VariableValue::BigUint(num), bits));
428    }
429
430    // Hex prefix (0x or 0X)
431    if cleaned.starts_with("0x") || cleaned.starts_with("0X") {
432        let hex_str = &cleaned[2..];
433        let num = BigUint::parse_bytes(hex_str.as_bytes(), 16)
434            .ok_or_else(|| MappingParseError::InvalidHex(token.to_string()))?;
435        // Actual number of bits required to represent the hex number
436        // We do not want a multiple of 4 here, but the actual bit width
437        let bits = num.bits() as u32;
438        return Ok((VariableValue::BigUint(num), bits));
439    }
440
441    // Decimal (default for numeric strings)
442    if let Some(num) = BigUint::parse_bytes(cleaned.as_bytes(), 10) {
443        let bits = num.bits() as u32;
444        return Ok((VariableValue::BigUint(num), bits));
445    }
446
447    Err(MappingParseError::InvalidStringCharacters {
448        value: token.to_string(),
449    })
450}
451
452/// Parse the first item into a [`VariableValue`], a [`ValueKind`], and its bit width.
453fn parse_key_with_kind(token: &str) -> Result<(VariableValue, ValueKind, u32), MappingParseError> {
454    // Check if token contains [kind] notation: value[kind]
455    if let Some(bracket_pos) = token.find('[')
456        && let Some(close_bracket) = token.find(']')
457        && close_bracket > bracket_pos
458    {
459        let value_part = &token[..bracket_pos];
460        let kind_part = &token[bracket_pos + 1..close_bracket];
461
462        let (value, len) = parse_key_value(value_part)?;
463        let kind = parse_color_kind(kind_part)?;
464
465        return Ok((value, kind, len));
466    }
467
468    // No [kind] notation, default to Normal
469    let (value, len) = parse_key_value(token)?;
470    Ok((value, ValueKind::Normal, len))
471}
472
473/// Parse a color or value kind from a token string.
474///
475/// This function supports:
476/// - Hex color codes in the format #RRGGBB or RRGGBB
477/// - Named colors from [`ecolor::Color32`]
478/// - Value kinds from [`surfer_translation_types::ValueKind`]
479fn parse_color_kind(token: &str) -> Result<ValueKind, MappingParseError> {
480    // Try hex color (#RRGGBB or RRGGBB)
481    let hex_str = token.strip_prefix('#').unwrap_or(token);
482
483    if hex_str.len() == 6 && hex_str.chars().all(|c| c.is_ascii_hexdigit()) {
484        let r = u8::from_str_radix(&hex_str[0..2], 16)
485            .map_err(|_| MappingParseError::InvalidHex(token.to_string()))?;
486        let g = u8::from_str_radix(&hex_str[2..4], 16)
487            .map_err(|_| MappingParseError::InvalidHex(token.to_string()))?;
488        let b = u8::from_str_radix(&hex_str[4..6], 16)
489            .map_err(|_| MappingParseError::InvalidHex(token.to_string()))?;
490        return Ok(ValueKind::Custom(Color32::from_rgb(r, g, b)));
491    }
492
493    // Try value kinds and ecolor::Color32 named colors using lookup table
494    let lower = token.to_lowercase();
495    kind_color_keywords()
496        .get(lower.as_str())
497        .copied()
498        .ok_or_else(|| MappingParseError::UnknownKindColor(token.to_string()))
499}
500
501// Check if line starts with keyword (case-insensitive) and parse value after '='
502fn parse_specifier(line: &str, keyword: &str) -> Option<String> {
503    // Check if line starts with keyword (case-insensitive)
504    if !(line.len() >= keyword.len() && line[..keyword.len()].eq_ignore_ascii_case(keyword)) {
505        return None;
506    }
507
508    let after_keyword = &line[keyword.len()..];
509
510    // Skip optional whitespace and look for '='
511    if let Some(rest) = after_keyword.trim_start().strip_prefix('=') {
512        // Found valid separator, extract and return the value
513        let value = rest.trim().to_string();
514        return Some(value);
515    }
516
517    None
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523    use camino::Utf8PathBuf;
524    use num::BigUint;
525
526    fn color_eq(a: Color32, b: Color32) -> bool {
527        a.r() == b.r() && a.g() == b.g() && a.b() == b.b() && a.a() == b.a()
528    }
529
530    #[test]
531    fn parse_empty_content_uses_default_name_and_no_bits() {
532        let map = parse_content_with_default_name("", Some("DefaultName".to_string())).unwrap();
533        assert_eq!(map.name, "DefaultName".to_string());
534        assert_eq!(map.bits, 0); // empty content => no bits
535        assert!(map.entries.is_empty());
536    }
537
538    #[test]
539    fn parse_with_name_bits_and_entries_binary_and_hex_and_colors() {
540        let content = "Name = MyMap\nBits = 4\n0[red] ZERO\n1[#00FF00] ONE\n0xA[blue] TEN";
541        let map = parse_content_with_default_name(content, None).unwrap();
542        assert_eq!(map.name, "MyMap".to_string());
543        assert_eq!(map.bits, 4);
544        // Expect numeric keys stored as BigUint
545        let zero = map
546            .entries
547            .get(&VariableValue::BigUint(BigUint::from(0u32)))
548            .expect("ZERO entry");
549        assert_eq!(zero.label, "ZERO");
550        assert_eq!(zero.kind, ValueKind::Custom(Color32::RED));
551        let one = map
552            .entries
553            .get(&VariableValue::BigUint(BigUint::from(1u32)))
554            .expect("ONE entry");
555        assert_eq!(one.label, "ONE");
556        // #00FF00 => green
557        assert_eq!(one.kind, ValueKind::Custom(Color32::GREEN));
558        let ten = map
559            .entries
560            .get(&VariableValue::BigUint(BigUint::from(10u32)))
561            .expect("TEN entry");
562        assert_eq!(ten.label, "TEN");
563        assert_eq!(ten.kind, ValueKind::Custom(Color32::BLUE));
564    }
565
566    #[test]
567    fn duplicate_keys_keep_first_and_log() {
568        let content = "Bits = 4\n0[red] ZERO\n0[blue] ZERO_DUP\n1[green] ONE";
569        let map = parse_content_with_default_name(content, None).unwrap();
570        // Only one entry for key 0000
571        assert_eq!(
572            map.entries
573                .get(&VariableValue::BigUint(BigUint::from(0u32)))
574                .unwrap()
575                .label,
576            "ZERO"
577        );
578        assert_eq!(
579            map.entries
580                .get(&VariableValue::BigUint(BigUint::from(1u32)))
581                .unwrap()
582                .label,
583            "ONE"
584        );
585        assert_eq!(map.entries.len(), 2);
586    }
587
588    #[test]
589    fn infer_bit_width_from_longest_entry() {
590        // Longest entry after normalization should determine width
591        let content = "3 THREE\n15 FIFTEEN"; // 3 => 11, 15 => 1111 so width=4
592        let map = parse_content_with_default_name(content, Some("Numbers".to_string())).unwrap();
593        assert_eq!(map.bits, 4);
594        assert!(
595            map.entries
596                .contains_key(&VariableValue::BigUint(BigUint::from(3u32)))
597        );
598        assert!(
599            map.entries
600                .contains_key(&VariableValue::BigUint(BigUint::from(15u32)))
601        );
602    }
603
604    #[test]
605    fn error_on_mismatched_string_length() {
606        // Bits = 3 but entry has 4 chars in first column
607        let content = "Bits =  3\n0bxxuu LABEL";
608        let err = parse_content_with_default_name(content, None).unwrap_err();
609        match err {
610            MappingParseError::StringLengthMismatch { expected, .. } => {
611                assert_eq!(expected, 3)
612            }
613            other => panic!("Unexpected error variant: {other}"),
614        }
615    }
616
617    #[test]
618    fn parse_line_allows_unquoted_label_with_spaces() {
619        let line = "0b0101 Label With Spaces";
620        let (val, label, kind, len) = parse_line(line).expect("parse line");
621        assert_eq!(label, "Label With Spaces");
622        assert_eq!(len, 4);
623        assert!(matches!(kind, ValueKind::Normal));
624        match val {
625            VariableValue::BigUint(v) => assert_eq!(v, BigUint::from(0b0101u32)),
626            other => panic!("unexpected value parsed: {other:?}"),
627        }
628    }
629
630    #[test]
631    fn parse_hex_and_decimal_numbers() {
632        let content = "Bits =  5\n0x1F[blue] HEXVAL\n7[green] DECVAL"; // 0x1F => 11111, 7 => 00111
633        let map = parse_content_with_default_name(content, None).unwrap();
634        assert_eq!(map.bits, 5);
635        assert!(
636            map.entries
637                .contains_key(&VariableValue::BigUint(BigUint::from(31u32)))
638        );
639        assert!(
640            map.entries
641                .contains_key(&VariableValue::BigUint(BigUint::from(7u32)))
642        );
643        assert_eq!(
644            map.entries
645                .get(&VariableValue::BigUint(BigUint::from(31u32)))
646                .unwrap()
647                .label,
648            "HEXVAL"
649        );
650        assert_eq!(
651            map.entries
652                .get(&VariableValue::BigUint(BigUint::from(7u32)))
653                .unwrap()
654                .label,
655            "DECVAL"
656        );
657    }
658
659    #[test]
660    fn mix_binary_and_decimal_without_bits_line_infers_width() {
661        // Binary token with 0b prefix plus decimal numbers; longest normalized width should be 4
662        let content = "0b0101 BINLABEL\n7 DECSEVEN\n13 DECTHIRTEEN"; // 0b0101 => 0101, 7 => 111, 13 => 1101
663        let map = parse_content_with_default_name(content, Some("Mixed".to_string())).unwrap();
664        assert_eq!(map.name, "Mixed".to_string());
665        assert_eq!(map.bits, 4);
666        assert!(
667            map.entries
668                .contains_key(&VariableValue::BigUint(BigUint::from(5u32)))
669        ); // binary preserved
670        assert!(
671            map.entries
672                .contains_key(&VariableValue::BigUint(BigUint::from(7u32)))
673        ); // 7 padded to 4 bits
674        assert!(
675            map.entries
676                .contains_key(&VariableValue::BigUint(BigUint::from(13u32)))
677        ); // 13 already 4 bits
678        assert_eq!(
679            map.entries
680                .get(&VariableValue::BigUint(BigUint::from(5u32)))
681                .unwrap()
682                .label,
683            "BINLABEL"
684        );
685        assert_eq!(
686            map.entries
687                .get(&VariableValue::BigUint(BigUint::from(7u32)))
688                .unwrap()
689                .label,
690            "DECSEVEN"
691        );
692        assert_eq!(
693            map.entries
694                .get(&VariableValue::BigUint(BigUint::from(13u32)))
695                .unwrap()
696                .label,
697            "DECTHIRTEEN"
698        );
699    }
700
701    #[test]
702    fn parse_binary_octal_hex_decimal_prefixes() {
703        // Test all number formats: 0b (binary), 0o (octal), 0x (hex), and decimal
704        let content =
705            "Bits =  8\n0b1111[red] BINARY\n0o17[green] OCTAL\n0xFF[blue] HEX\n255[yellow] DECIMAL";
706        let map = parse_content_with_default_name(content, None).unwrap();
707        assert_eq!(map.bits, 8);
708        // 0b1111 => 00001111
709        assert!(
710            map.entries
711                .contains_key(&VariableValue::BigUint(BigUint::from(15u32)))
712        );
713        assert_eq!(
714            map.entries
715                .get(&VariableValue::BigUint(BigUint::from(15u32)))
716                .unwrap()
717                .label,
718            "BINARY"
719        );
720        // 0o17 => 15 decimal => 00001111 (same as 0b1111, duplicate so keeps first)
721        assert_eq!(
722            map.entries
723                .get(&VariableValue::BigUint(BigUint::from(15u32)))
724                .unwrap()
725                .label,
726            "BINARY"
727        );
728        // 0xFF => 255 decimal => 11111111
729        assert!(
730            map.entries
731                .contains_key(&VariableValue::BigUint(BigUint::from(255u32)))
732        );
733        assert_eq!(
734            map.entries
735                .get(&VariableValue::BigUint(BigUint::from(255u32)))
736                .unwrap()
737                .label,
738            "HEX"
739        );
740        // 255 decimal => 11111111 (same as 0xFF, duplicate so keeps first)
741        assert_eq!(
742            map.entries
743                .get(&VariableValue::BigUint(BigUint::from(255u32)))
744                .unwrap()
745                .label,
746            "HEX"
747        );
748    }
749
750    #[test]
751    fn file_based_translator_basic_translate_and_fallback() {
752        use std::time::{SystemTime, UNIX_EPOCH};
753        // Unique temp file path
754        let ts = SystemTime::now()
755            .duration_since(UNIX_EPOCH)
756            .unwrap()
757            .as_nanos();
758        let mut pathbuf = std::env::temp_dir();
759        pathbuf.push(format!("mapping_test_{}.txt", ts));
760        let path = Utf8PathBuf::from_path_buf(pathbuf).expect("temp path must be UTF-8");
761
762        let content = "Name = ExampleMapping\nBits =  4\n0[red] ZERO\n1[green] ONE\n2[blue] TWO";
763        std::fs::write(path.as_std_path(), content).expect("write mapping file");
764
765        let translator = MappingTranslator::new_from_file(&path)
766            .ok()
767            .expect("create translator");
768        assert_eq!(translator.name(), "ExampleMapping");
769
770        // Match padded binary for string value
771        let (label_one, kind_one) =
772            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(1u32)));
773        assert_eq!(label_one, "ONE");
774        match kind_one {
775            ValueKind::Custom(c) => assert!(color_eq(c, Color32::GREEN)),
776            _ => panic!("expected custom green"),
777        }
778
779        // Integer value translation (BigUint) should pad and map to label TWO
780        let (label_two, kind_two) =
781            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(2u32)));
782        assert_eq!(label_two, "TWO");
783        match kind_two {
784            ValueKind::Custom(c) => assert!(color_eq(c, Color32::BLUE)),
785            _ => panic!("expected custom blue"),
786        }
787
788        // Fallback for value not in map (all-defined binary -> Normal)
789        let (label_unknown, kind_unknown) =
790            translator.basic_translate(4, &VariableValue::String("0011".into()));
791        assert_eq!(label_unknown, "0011");
792        assert!(matches!(kind_unknown, ValueKind::Normal));
793
794        // Value with x should return Error
795        let (label_undef, kind_undef) =
796            translator.basic_translate(4, &VariableValue::String("00x1".into()));
797        assert_eq!(label_undef, "00x1");
798        assert!(matches!(kind_undef, ValueKind::Undef));
799
800        // Value with u char should return Undef
801        let (label_undef, kind_undef) =
802            translator.basic_translate(4, &VariableValue::String("00u1".into()));
803        assert_eq!(label_undef, "00u1");
804        assert!(matches!(kind_undef, ValueKind::Undef));
805    }
806
807    #[test]
808    fn filename_derived_name_when_no_name_line_present() {
809        use std::time::{SystemTime, UNIX_EPOCH};
810        let ts = SystemTime::now()
811            .duration_since(UNIX_EPOCH)
812            .unwrap()
813            .as_micros();
814        let stem = format!("auto_file_name_{}", ts);
815        let mut pathbuf = std::env::temp_dir();
816        pathbuf.push(format!("{stem}.mapping"));
817
818        // No Name line, first line is Bits so default name should be file stem
819        let content = "Bits=3\n0[red] ZERO\n1[green] ONE\n2[blue] TWO";
820        let path = Utf8PathBuf::from_path_buf(pathbuf).expect("temp path must be UTF-8");
821        std::fs::write(path.as_std_path(), content).expect("write mapping file");
822        let translator = MappingTranslator::new_from_file(&path)
823            .ok()
824            .expect("create translator");
825        assert_eq!(translator.name(), stem); // derived from filename
826        assert_eq!(translator.map.bits, 3);
827        // Translate a BigUint value 2 => binary 10 padded to 3 bits -> 010 maps to TWO
828        let (label_two, kind_two) =
829            translator.basic_translate(3, &VariableValue::BigUint(BigUint::from(2u32)));
830        assert_eq!(label_two, "TWO");
831        match kind_two {
832            ValueKind::Custom(c) => assert!(color_eq(c, Color32::BLUE)),
833            _ => panic!("expected custom blue"),
834        }
835
836        // Value not present (binary 111) should fallback to Normal
837        let (fallback, vk_fb) = translator.basic_translate(3, &VariableValue::String("111".into()));
838        assert_eq!(fallback, "111");
839        assert!(matches!(vk_fb, ValueKind::Normal));
840
841        // Value with z should be HighImp
842        let (high_imp_val, high_imp_kind) =
843            translator.basic_translate(3, &VariableValue::String("1z1".into()));
844        assert_eq!(high_imp_val, "1z1");
845        assert!(matches!(high_imp_kind, ValueKind::HighImp));
846    }
847
848    #[test]
849    fn comments_and_blank_lines_are_ignored() {
850        let content = "# top comment\n   # another comment\n\nName=Commented\n# mid\nBits =  3\n# entry comment\n0[red] ZERO\n# Hash comment\n\n1[green] ONE\n   # trailing comment line\n2[blue] TWO";
851        let map = parse_content_with_default_name(content, None).unwrap();
852        assert_eq!(map.name, "Commented".to_string());
853        assert_eq!(map.bits, 3);
854        assert_eq!(map.entries.len(), 3);
855        assert_eq!(
856            map.entries
857                .get(&VariableValue::BigUint(BigUint::from(0u32)))
858                .unwrap()
859                .label,
860            "ZERO"
861        );
862        assert_eq!(
863            map.entries
864                .get(&VariableValue::BigUint(BigUint::from(1u32)))
865                .unwrap()
866                .label,
867            "ONE"
868        );
869        assert_eq!(
870            map.entries
871                .get(&VariableValue::BigUint(BigUint::from(2u32)))
872                .unwrap()
873                .label,
874            "TWO"
875        );
876    }
877
878    #[test]
879    fn parse_and_translate_valuekind_keywords() {
880        use std::time::{SystemTime, UNIX_EPOCH};
881
882        let ts = SystemTime::now()
883            .duration_since(UNIX_EPOCH)
884            .unwrap()
885            .as_nanos();
886        let mut pathbuf = std::env::temp_dir();
887        pathbuf.push(format!("mapping_kinds_{}.txt", ts));
888        let path = Utf8PathBuf::from_path_buf(pathbuf).expect("temp path must be UTF-8");
889
890        // Define entries with various ValueKind keywords
891        let content = "Bits =  4\n0[warn] ZERO\n1[undef] ONE\n2[highimp] TWO\n3[dontcare] THREE\n4[weak] FOUR\n5[error] FIVE\n6[normal] SIX";
892        std::fs::write(path.as_std_path(), content).expect("write mapping file");
893
894        let translator = MappingTranslator::new_from_file(&path)
895            .ok()
896            .expect("create translator");
897        assert_eq!(translator.map.bits, 4);
898
899        // Test each ValueKind keyword
900        let (label_zero, kind_zero) =
901            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(0u32)));
902        assert_eq!(label_zero, "ZERO");
903        assert!(matches!(kind_zero, ValueKind::Warn));
904
905        let (label_one, kind_one) =
906            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(1u32)));
907        assert_eq!(label_one, "ONE");
908        assert!(matches!(kind_one, ValueKind::Undef));
909
910        let (label_two, kind_two) =
911            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(2u32)));
912        assert_eq!(label_two, "TWO");
913        assert!(matches!(kind_two, ValueKind::HighImp));
914
915        let (label_three, kind_three) =
916            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(3u32)));
917        assert_eq!(label_three, "THREE");
918        assert!(matches!(kind_three, ValueKind::DontCare));
919
920        let (label_four, kind_four) =
921            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(4u32)));
922        assert_eq!(label_four, "FOUR");
923        assert!(matches!(kind_four, ValueKind::Weak));
924
925        let (label_five, kind_five) =
926            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(5u32)));
927        assert_eq!(label_five, "FIVE");
928        assert!(matches!(kind_five, ValueKind::Error));
929
930        let (label_six, kind_six) =
931            translator.basic_translate(4, &VariableValue::BigUint(BigUint::from(6u32)));
932        assert_eq!(label_six, "SIX");
933        assert!(matches!(kind_six, ValueKind::Normal));
934    }
935
936    #[test]
937    fn error_variants_coverage() {
938        // InvalidHex via parse_first_column
939        match parse_key_value("0xZZ") {
940            Err(MappingParseError::InvalidHex(tok)) => assert_eq!(tok, "0xZZ"),
941            other => panic!("expected InvalidHex, got: {:?}", other),
942        }
943
944        // UnknownColor via parse_color_kind
945        match parse_color_kind("not_a_color") {
946            Err(MappingParseError::UnknownKindColor(s)) => assert_eq!(s, "not_a_color"),
947            other => panic!("expected UnknownColor, got: {:?}", other),
948        }
949
950        // BinaryTooWide
951        match normalize_first_column(&VariableValue::String("1111".into()), 4, 3) {
952            Err(MappingParseError::BinaryTooWide {
953                value,
954                required,
955                specified,
956            }) => {
957                assert_eq!(value, "1111");
958                assert!(required > specified);
959            }
960            other => panic!("expected BinaryTooWide, got: {:?}", other),
961        }
962
963        // StringLengthMismatch
964        match normalize_first_column(&VariableValue::String("ABCD".into()), 4, 3) {
965            Err(MappingParseError::StringLengthMismatch {
966                value,
967                value_len,
968                expected,
969            }) => {
970                assert_eq!(value, "ABCD");
971                assert_eq!(value_len, 4);
972                assert_eq!(expected, 3);
973            }
974            other => panic!("expected StringLengthMismatch, got: {:?}", other),
975        }
976
977        // MissingSecondColumn and EmptyLine via parse_line
978        match parse_line("") {
979            Err(MappingParseError::EmptyLine) => {}
980            other => panic!("expected EmptyLine, got: {:?}", other),
981        }
982
983        match parse_line("0101") {
984            Err(MappingParseError::MissingMapping) => {}
985            other => panic!("expected MissingSecondColumn, got: {:?}", other),
986        }
987
988        // LineError produced by parse_content_with_default_name when an entry line is invalid
989        let content = "Bits =  4\n0 ZERO red\nBADLINE";
990        match parse_content_with_default_name(content, None) {
991            Err(MappingParseError::LineError {
992                line,
993                content,
994                message: _,
995            }) => {
996                assert_eq!(line, 3);
997                assert_eq!(content, "BADLINE");
998            }
999            other => panic!("expected LineError, got: {:?}", other),
1000        }
1001
1002        // Io error via MappingTranslatorMap::new with non-existent file
1003        let pathbuf = std::path::PathBuf::from("/this/path/should/not/exist/mapping.tmp");
1004        let path = Utf8PathBuf::from_path_buf(pathbuf).expect("path must be UTF-8");
1005        match MappingTranslatorMap::new(path) {
1006            Err(MappingParseError::Io(_)) => {}
1007            other => panic!("expected Io, got: {:?}", other),
1008        }
1009    }
1010}