Skip to main content

firmion_map_phase/
map_phase.rs

1// Output map construction for firmion.
2//
3// MapDb collects the semantic payload for all map output formats. All data
4// derives from the LocationDb and IRDb after layout_phase completes.
5//
6
7// Don't clutter upstream docs.rs for an otherwise private library.
8#![doc(hidden)]
9
10use diags::Diags;
11use ir::IRKind;
12use ir::ParameterValue;
13use irdb::IRDb;
14use locationdb::LocationDb;
15use mapdb::{ConstEntry, LabelEntry, MapDb, SectionEntry};
16
17#[allow(unused_imports)]
18use tracing::{debug, error, info, trace, warn};
19
20// -- Private formatting helpers -----------------------------------------------
21
22/// Returns the minimum name-column width: at least 16, at least as wide as
23/// the longest name in `names`.
24fn name_col_width<'a>(names: impl Iterator<Item = &'a str>) -> usize {
25    names.map(str::len).max().unwrap_or(0).max(16)
26}
27
28/// Renders a `ParameterValue` as a human-readable string.
29///   U64      → 0x0000000000001000
30///   I64      → -42
31///   Integer  → 42
32///   String   → "hello"
33pub fn fmt_const_value(pv: &ParameterValue) -> String {
34    match pv {
35        ParameterValue::U64(v) => format!("0x{v:016x}"),
36        ParameterValue::I64(v) => format!("{v}"),
37        ParameterValue::Integer(v) => format!("{v}"),
38        ParameterValue::QuotedString(s) => format!("\"{s}\""),
39        ParameterValue::Identifier(s) | ParameterValue::DeferredRef(s) => s.clone(),
40        ParameterValue::Extension => "(extension)".to_string(),
41        ParameterValue::Unknown => "(unknown)".to_string(),
42    }
43}
44
45// -- CSV formatter --------------------------------------------------
46
47/// Renders `map` as a CSV map.
48///
49/// Format overview:
50/// ```text
51/// Output File, output.bin
52/// Base Address, 0x0000000000001000
53/// Total Size (hex), 0x0000000000000050
54/// Total Size (decimal), 80
55///
56/// Constants
57/// Name,            Value,
58/// BASE,            0x0000000000001000,
59///
60/// Sections
61/// Name,            Address,             Offset,              File Offset,         Size (bytes),
62/// foo,             0x0000000000001000,  0x0000000000000000,  0x0000000000000000,  50,
63///
64/// Labels
65/// Name,            Address,             Offset,              File Offset,
66/// lab1,            0x0000000000001004,  0x0000000000000004,  0x0000000000000004,
67/// ```
68pub fn format_csv(map: &MapDb) -> String {
69    use std::fmt::Write;
70    let mut out = String::new();
71
72    // -- Header ----------------------------------------------------------------
73    writeln!(out, "Output File, {}", map.output_file).unwrap();
74    writeln!(out, "Base Address, 0x{:016x}", map.base_addr).unwrap();
75    writeln!(out, "Total Size (hex), 0x{:016x}", map.total_size).unwrap();
76    writeln!(out, "Total Size (decimal), {}", map.total_size).unwrap();
77
78    // -- Constants -------------------------------------------------------------
79    writeln!(out).unwrap();
80    writeln!(out, "Constants").unwrap();
81    if map.consts.is_empty() {
82        writeln!(out, "  (none)").unwrap();
83    } else {
84        let name_w = name_col_width(map.consts.iter().map(|c| c.name.as_str()));
85        writeln!(out, "{:<name_w$},  {:<20},  Used,", "Name,", "Value,").unwrap();
86        for c in &map.consts {
87            writeln!(
88                out,
89                "{:<name_w$},  {:<20},  {},",
90                c.name,
91                fmt_const_value(&c.value),
92                if c.used { "yes" } else { "no" }
93            )
94            .unwrap();
95        }
96    }
97
98    // -- Sections --------------------------------------------------------------
99    writeln!(out).unwrap();
100    writeln!(out, "Sections").unwrap();
101    if map.sections.is_empty() {
102        writeln!(out, "  (none)").unwrap();
103    } else {
104        let name_w = name_col_width(map.sections.iter().map(|s| s.name.as_str()));
105        writeln!(
106            out,
107            "{:<name_w$},  {:<18},  {:<18},  {:<18},  Size (bytes),",
108            "Name", "Address", "Offset", "File Offset"
109        )
110        .unwrap();
111        for s in &map.sections {
112            writeln!(
113                out,
114                "{:<name_w$},  0x{:016x},  0x{:016x},  0x{:016x},  {},",
115                s.name, s.abs_start, s.off, s.file_offset, s.size
116            )
117            .unwrap();
118        }
119    }
120
121    // -- Labels ----------------------------------------------------------------
122    writeln!(out).unwrap();
123    writeln!(out, "Labels").unwrap();
124    if map.labels.is_empty() {
125        writeln!(out, "  (none)").unwrap();
126    } else {
127        let name_w = name_col_width(map.labels.iter().map(|l| l.name.as_str()));
128        writeln!(
129            out,
130            "{:<name_w$},  {:<18},  {:<18},  File Offset,",
131            "Name,", "Address,", "Offset,"
132        )
133        .unwrap();
134        for l in &map.labels {
135            writeln!(
136                out,
137                "{:<name_w$},  0x{:016x},  0x{:016x},  0x{:016x},",
138                l.name, l.abs_addr, l.off, l.file_offset
139            )
140            .unwrap();
141        }
142    }
143
144    out
145}
146
147/// Produces a C99 preprocessor compatible `.h` header format output
148pub fn format_c99(map: &MapDb) -> String {
149    use std::fmt::Write;
150    let mut out = String::new();
151
152    let stem = std::path::Path::new(&map.output_file)
153        .file_stem()
154        .and_then(|s| s.to_str())
155        .unwrap_or("OUTPUT")
156        .to_uppercase()
157        .replace(|c: char| !c.is_ascii_alphanumeric(), "_");
158
159    writeln!(out, "// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!").unwrap();
160    writeln!(out, "// Automatically generated file! Do not edit!").unwrap();
161    writeln!(out, "// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!").unwrap();
162    writeln!(out, "#ifndef {}_MAP_H", stem).unwrap();
163    writeln!(out, "#define {}_MAP_H\n", stem).unwrap();
164
165    writeln!(
166        out,
167        "#define {}_MAP_BASE_ADDR 0x{:016x}ULL",
168        stem, map.base_addr
169    )
170    .unwrap();
171    writeln!(out, "#define {}_MAP_TOTAL_SIZE {}ULL", stem, map.total_size).unwrap();
172
173    if !map.sections.is_empty() {
174        writeln!(out, "\n// Sections").unwrap();
175        for sec in &map.sections {
176            let sec_name = sec.name.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
177            writeln!(
178                out,
179                "#define {}_MAP_{}_ADDR 0x{:016x}ULL",
180                stem, sec_name, sec.abs_start
181            )
182            .unwrap();
183            writeln!(
184                out,
185                "#define {}_MAP_{}_OFFSET 0x{:016x}ULL",
186                stem, sec_name, sec.off
187            )
188            .unwrap();
189            writeln!(
190                out,
191                "#define {}_MAP_{}_FILE_OFFSET 0x{:016x}ULL",
192                stem, sec_name, sec.file_offset
193            )
194            .unwrap();
195            writeln!(
196                out,
197                "#define {}_MAP_{}_SIZE {}ULL",
198                stem, sec_name, sec.size
199            )
200            .unwrap();
201            writeln!(out).unwrap();
202        }
203    }
204
205    if !map.labels.is_empty() {
206        writeln!(out, "// Labels").unwrap();
207        for lab in &map.labels {
208            let lab_name = lab.name.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
209            writeln!(
210                out,
211                "#define {}_MAP_{}_ADDR 0x{:016x}ULL",
212                stem, lab_name, lab.abs_addr
213            )
214            .unwrap();
215            writeln!(
216                out,
217                "#define {}_MAP_{}_OFFSET 0x{:016x}ULL",
218                stem, lab_name, lab.off
219            )
220            .unwrap();
221            writeln!(
222                out,
223                "#define {}_MAP_{}_FILE_OFFSET 0x{:016x}ULL",
224                stem, lab_name, lab.file_offset
225            )
226            .unwrap();
227            writeln!(out).unwrap();
228        }
229    }
230
231    writeln!(out, "\n#endif").unwrap();
232    out
233}
234
235// -- JSON formatter ------------------------------------------------------------
236
237/// Renders `map` as a pretty-printed JSON string.
238///
239/// Addresses and offsets are hex strings (`"0x..."`) for readability.
240/// Sizes and the total are plain JSON numbers.
241/// Const values use the same string representation as `format_csv`.
242///
243/// ```json
244/// {
245///   "output_file": "output.bin",
246///   "base_addr": "0x0000000000001000",
247///   "total_size": 80,
248///   "constants": [
249///     { "name": "BASE", "value": "0x0000000000001000" }
250///   ],
251///   "sections": [
252///     { "name": "text", "address": "0x0000000000001000",
253///       "offset": "0x0000000000000000",
254///       "file_offset": "0x0000000000000000", "size": 50 }
255///   ],
256///   "labels": [
257///     { "name": "start", "address": "0x0000000000001000",
258///       "offset": "0x0000000000000000",
259///       "file_offset": "0x0000000000000000" }
260///   ]
261/// }
262/// ```
263pub fn format_json(map: &MapDb) -> String {
264    use serde_json::{Value, json};
265
266    let constants: Vec<Value> = map
267        .consts
268        .iter()
269        .map(|c| json!({ "name": c.name, "value": fmt_const_value(&c.value), "used": c.used }))
270        .collect();
271
272    let sections: Vec<Value> = map
273        .sections
274        .iter()
275        .map(|s| {
276            json!({
277                "name":        s.name,
278                "address":     format!("0x{:016x}", s.abs_start),
279                "offset":      format!("0x{:016x}", s.off),
280                "file_offset": format!("0x{:016x}", s.file_offset),
281                "size":        s.size,
282            })
283        })
284        .collect();
285
286    let labels: Vec<Value> = map
287        .labels
288        .iter()
289        .map(|l| {
290            json!({
291                "name":        l.name,
292                "address":     format!("0x{:016x}", l.abs_addr),
293                "offset":      format!("0x{:016x}", l.off),
294                "file_offset": format!("0x{:016x}", l.file_offset),
295            })
296        })
297        .collect();
298
299    let root = json!({
300        "output_file": map.output_file,
301        "base_addr":   format!("0x{:016x}", map.base_addr),
302        "total_size":  map.total_size,
303        "constants":   constants,
304        "sections":    sections,
305        "labels":      labels,
306    });
307
308    serde_json::to_string_pretty(&root).expect("JSON serialization failed")
309}
310
311// -- Rust formatter ------------------------------------------------------------
312
313/// Produces a Rust module format output containing static address mappings natively.
314pub fn format_rs(map: &MapDb) -> String {
315    use std::fmt::Write;
316    let mut out = String::new();
317
318    let stem = std::path::Path::new(&map.output_file)
319        .file_stem()
320        .and_then(|s| s.to_str())
321        .unwrap_or("OUTPUT")
322        .replace(|c: char| !c.is_ascii_alphanumeric(), "_");
323
324    writeln!(out, "// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!").unwrap();
325    writeln!(out, "// Automatically generated file! Do not edit!").unwrap();
326    writeln!(out, "// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!").unwrap();
327    writeln!(out, "pub mod {}_map {{", stem).unwrap();
328    writeln!(out, "    #![allow(dead_code)]\n").unwrap(); // Prevent compilation warnings if endpoints are unused
329
330    writeln!(
331        out,
332        "    pub const BASE_ADDR: u64 = 0x{:016x};",
333        map.base_addr
334    )
335    .unwrap();
336    writeln!(out, "    pub const TOTAL_SIZE: u64 = {};", map.total_size).unwrap();
337
338    if !map.consts.is_empty() {
339        writeln!(out, "\n    // Constants").unwrap();
340        for c in &map.consts {
341            let rs_val = match &c.value {
342                ParameterValue::U64(v) => format!("0x{:016x}", v),
343                ParameterValue::I64(v) => format!("{}", v),
344                ParameterValue::Integer(v) => format!("{}", v),
345                ParameterValue::QuotedString(s) => format!("\"{}\"", s),
346                _ => continue,
347            };
348
349            let rs_type = match &c.value {
350                ParameterValue::U64(_) => "u64",
351                ParameterValue::I64(_) => "i64",
352                ParameterValue::Integer(_) => "i64", // default generic integers to signed equivalent to standard C mappings unless explicit
353                ParameterValue::QuotedString(_) => "&str",
354                _ => continue,
355            };
356
357            let name = c
358                .name
359                .replace(|c: char| !c.is_ascii_alphanumeric(), "_")
360                .to_uppercase();
361            writeln!(out, "    pub const {}: {} = {};", name, rs_type, rs_val).unwrap();
362        }
363    }
364
365    if !map.sections.is_empty() {
366        writeln!(out, "\n    // Sections").unwrap();
367        for sec in &map.sections {
368            let sec_name = sec
369                .name
370                .replace(|c: char| !c.is_ascii_alphanumeric(), "_")
371                .to_uppercase();
372            writeln!(
373                out,
374                "    pub const {}_ADDR: u64 = 0x{:016x};",
375                sec_name, sec.abs_start
376            )
377            .unwrap();
378            writeln!(
379                out,
380                "    pub const {}_OFFSET: u64 = 0x{:016x};",
381                sec_name, sec.off
382            )
383            .unwrap();
384            writeln!(
385                out,
386                "    pub const {}_FILE_OFFSET: u64 = 0x{:016x};",
387                sec_name, sec.file_offset
388            )
389            .unwrap();
390            writeln!(out, "    pub const {}_SIZE: u64 = {};", sec_name, sec.size).unwrap();
391            writeln!(out).unwrap();
392        }
393    }
394
395    if !map.labels.is_empty() {
396        writeln!(out, "    // Labels").unwrap();
397        for lab in &map.labels {
398            let lab_name = lab
399                .name
400                .replace(|c: char| !c.is_ascii_alphanumeric(), "_")
401                .to_uppercase();
402            writeln!(
403                out,
404                "    pub const {}_ADDR: u64 = 0x{:016x};",
405                lab_name, lab.abs_addr
406            )
407            .unwrap();
408            writeln!(
409                out,
410                "    pub const {}_OFFSET: u64 = 0x{:016x};",
411                lab_name, lab.off
412            )
413            .unwrap();
414            writeln!(
415                out,
416                "    pub const {}_FILE_OFFSET: u64 = 0x{:016x};",
417                lab_name, lab.file_offset
418            )
419            .unwrap();
420            writeln!(out).unwrap();
421        }
422    }
423
424    writeln!(out, "}}").unwrap();
425    out
426}
427
428// -- MapDb construction --------------------------------------------------------
429
430pub fn build(
431    location_db: &LocationDb,
432    irdb: &IRDb,
433    output_file: &str,
434    _diags: &mut Diags,
435) -> MapDb {
436    let mut sections: Vec<SectionEntry> = Vec::new();
437    let mut labels: Vec<LabelEntry> = Vec::new();
438
439    let mut stack: Vec<(String, usize)> = Vec::new();
440    for (ir_index, ir) in irdb.ir_vec.iter().enumerate() {
441        match ir.kind {
442            IRKind::SectionStart => {
443                let name = irdb.get_opnd_as_identifier(ir, 0).to_string();
444                stack.push((name, ir_index));
445            }
446            IRKind::SectionEnd => {
447                let (name, start_ir) = stack.pop().unwrap();
448                let file_start = location_db.ir_locs[start_ir].file_offset;
449                let file_end = location_db.ir_locs[ir_index].file_offset;
450
451                let content_idx = ((start_ir + 1)..ir_index)
452                    .find(|&j| irdb.ir_vec[j].kind != IRKind::SetAddr)
453                    .unwrap_or(ir_index);
454                let content_loc = &location_db.ir_locs[content_idx];
455
456                sections.push(SectionEntry {
457                    name,
458                    file_offset: file_start,
459                    off: content_loc.addr.addr_offset,
460                    abs_start: content_loc.addr.addr_base + content_loc.addr.addr_offset,
461                    size: file_end - file_start,
462                });
463            }
464            IRKind::Label => {
465                let name = irdb.get_opnd_as_identifier(ir, 0).to_string();
466                let loc = &location_db.ir_locs[ir_index];
467                labels.push(LabelEntry {
468                    name,
469                    file_offset: loc.file_offset,
470                    off: loc.addr.addr_offset,
471                    abs_addr: loc.addr.addr_base + loc.addr.addr_offset,
472                });
473            }
474            _ => {}
475        }
476    }
477
478    let mut consts: Vec<ConstEntry> = irdb
479        .symbol_table
480        .iter_defined_with_used()
481        .map(|(name, pv, used)| ConstEntry {
482            name: name.to_string(),
483            value: pv.clone(),
484            used,
485        })
486        .collect();
487    consts.sort_by(|a, b| a.name.cmp(&b.name));
488
489    let base_addr = sections.first().map(|s| s.abs_start).unwrap_or(0);
490    let total_size = sections.last().map(|s| s.file_offset + s.size).unwrap_or(0);
491
492    MapDb {
493        output_file: output_file.to_string(),
494        base_addr,
495        total_size,
496        sections,
497        labels,
498        consts,
499    }
500}
501// -- Unit tests ----------------------------------------------------------------
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use ir::ParameterValue;
507
508    fn make_map() -> MapDb {
509        MapDb {
510            output_file: "out.bin".to_string(),
511            base_addr: 0x1000,
512            total_size: 0x80,
513            sections: vec![
514                SectionEntry {
515                    name: "text".to_string(),
516                    file_offset: 0x00,
517                    off: 0x00,
518                    abs_start: 0x1000,
519                    size: 0x40,
520                },
521                SectionEntry {
522                    name: "data".to_string(),
523                    file_offset: 0x40,
524                    off: 0x40,
525                    abs_start: 0x1040,
526                    size: 0x40,
527                },
528            ],
529            labels: vec![
530                LabelEntry {
531                    name: "start".to_string(),
532                    file_offset: 0x00,
533                    off: 0x00,
534                    abs_addr: 0x1000,
535                },
536                LabelEntry {
537                    name: "end_marker".to_string(),
538                    file_offset: 0x7f,
539                    off: 0x7f,
540                    abs_addr: 0x107f,
541                },
542            ],
543            consts: vec![
544                ConstEntry {
545                    name: "BASE".to_string(),
546                    value: ParameterValue::U64(0x1000),
547                    used: true,
548                },
549                ConstEntry {
550                    name: "COUNT".to_string(),
551                    value: ParameterValue::Integer(42),
552                    used: true,
553                },
554                ConstEntry {
555                    name: "VERSION".to_string(),
556                    value: ParameterValue::QuotedString("v1.0".to_string()),
557                    used: true,
558                },
559                ConstEntry {
560                    name: "OFFSET".to_string(),
561                    value: ParameterValue::I64(-10),
562                    used: true,
563                },
564            ],
565        }
566    }
567
568    #[test]
569    fn header_contains_output_file_and_base_addr() {
570        let out = format_csv(&make_map());
571        assert!(out.contains("out.bin"), "output file name missing");
572        assert!(out.contains("0x0000000000001000"), "base addr missing");
573    }
574
575    #[test]
576    fn header_contains_total_size() {
577        let out = format_csv(&make_map());
578        assert!(out.contains("128"), "total size in bytes missing");
579    }
580
581    #[test]
582    fn sections_contain_names_and_addresses() {
583        let out = format_csv(&make_map());
584        assert!(out.contains("text"), "section name 'text' missing");
585        assert!(out.contains("data"), "section name 'data' missing");
586        // abs_start of 'text' section
587        assert!(
588            out.contains("0x0000000000001000"),
589            "'text' abs_start missing"
590        );
591        // abs_start of 'data' section
592        assert!(
593            out.contains("0x0000000000001040"),
594            "'data' abs_start missing"
595        );
596    }
597
598    #[test]
599    fn sections_contain_sizes() {
600        let out = format_csv(&make_map());
601        assert!(out.contains("64,"), "section size '64' missing");
602    }
603
604    #[test]
605    fn labels_contain_names_and_addresses() {
606        let out = format_csv(&make_map());
607        assert!(out.contains("start"), "label 'start' missing");
608        assert!(out.contains("end_marker"), "label 'end_marker' missing");
609        assert!(
610            out.contains("0x000000000000107f"),
611            "label 'end_marker' abs_addr missing"
612        );
613    }
614
615    #[test]
616    fn consts_appear_before_sections_in_output() {
617        let out = format_csv(&make_map());
618        let const_pos = out.find("Constants").expect("Constants section missing");
619        let section_pos = out.find("Sections").expect("Sections section missing");
620        assert!(
621            const_pos < section_pos,
622            "Constants must appear before Sections"
623        );
624    }
625
626    #[test]
627    fn consts_contain_names_and_values() {
628        let out = format_csv(&make_map());
629        assert!(out.contains("BASE"), "const name 'BASE' missing");
630        assert!(out.contains("COUNT"), "const name 'COUNT' missing");
631        assert!(out.contains("OFFSET"), "const name 'OFFSET' missing");
632        assert!(out.contains("VERSION"), "const name 'VERSION' missing");
633        // U64 renders as hex
634        assert!(
635            out.contains("0x0000000000001000"),
636            "const U64 hex value missing"
637        );
638        // Integer renders as decimal
639        assert!(out.contains("42"), "const Integer decimal value missing");
640        // I64 renders as negative decimal string
641        assert!(out.contains("-10"), "const I64 neg value missing");
642        // String renders inside quotes
643        assert!(out.contains("\"v1.0\""), "const QuotedString value missing");
644        // used consts show "yes"
645        assert!(out.contains("yes"), "used column 'yes' missing");
646    }
647
648    #[test]
649    fn consts_unused_shows_no() {
650        let map = MapDb {
651            output_file: "x.bin".to_string(),
652            base_addr: 0,
653            total_size: 0,
654            sections: vec![],
655            labels: vec![],
656            consts: vec![ConstEntry {
657                name: "UNUSED".to_string(),
658                value: ParameterValue::U64(0),
659                used: false,
660            }],
661        };
662        let out = format_csv(&map);
663        assert!(
664            out.contains("no"),
665            "used column 'no' missing for unused const"
666        );
667    }
668
669    #[test]
670    fn empty_sections_shows_none() {
671        let map = MapDb {
672            output_file: "x.bin".to_string(),
673            base_addr: 0,
674            total_size: 0,
675            sections: vec![],
676            labels: vec![],
677            consts: vec![],
678        };
679        let out = format_csv(&map);
680        // Each table should report (none) when empty
681        assert_eq!(
682            out.matches("(none)").count(),
683            3,
684            "expected (none) for each empty table"
685        );
686    }
687
688    #[test]
689    fn repeated_section_name_appears_multiple_times() {
690        let map = MapDb {
691            output_file: "y.bin".to_string(),
692            base_addr: 0,
693            total_size: 0x20,
694            sections: vec![
695                SectionEntry {
696                    name: "foo".to_string(),
697                    file_offset: 0x00,
698                    off: 0x00,
699                    abs_start: 0x00,
700                    size: 0x10,
701                },
702                SectionEntry {
703                    name: "foo".to_string(),
704                    file_offset: 0x10,
705                    off: 0x10,
706                    abs_start: 0x10,
707                    size: 0x10,
708                },
709            ],
710            labels: vec![],
711            consts: vec![],
712        };
713        let out = format_csv(&map);
714        assert_eq!(
715            out.matches("foo").count(),
716            2,
717            "repeated section 'foo' should appear twice"
718        );
719    }
720
721    #[test]
722    fn fmt_const_value_variants() {
723        assert_eq!(
724            fmt_const_value(&ParameterValue::U64(0x10)),
725            "0x0000000000000010"
726        );
727        assert_eq!(fmt_const_value(&ParameterValue::I64(-7)), "-7");
728        assert_eq!(fmt_const_value(&ParameterValue::Integer(99)), "99");
729        assert_eq!(
730            fmt_const_value(&ParameterValue::QuotedString("hi".to_string())),
731            "\"hi\""
732        );
733    }
734
735    // -- format_json tests -----------------------------------------------------
736
737    #[test]
738    fn json_is_valid_and_contains_output_file() {
739        let out = format_json(&make_map());
740        let v: serde_json::Value = serde_json::from_str(&out).expect("output is not valid JSON");
741        assert_eq!(v["output_file"], "out.bin");
742    }
743
744    #[test]
745    fn json_header_fields() {
746        let out = format_json(&make_map());
747        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
748        assert_eq!(v["base_addr"], "0x0000000000001000");
749        assert_eq!(v["total_size"], 0x80u64);
750    }
751
752    #[test]
753    fn json_sections_contain_names_and_addresses() {
754        let out = format_json(&make_map());
755        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
756        let sections = v["sections"].as_array().unwrap();
757        let names: Vec<&str> = sections
758            .iter()
759            .map(|s| s["name"].as_str().unwrap())
760            .collect();
761        assert!(names.contains(&"text"), "section 'text' missing");
762        assert!(names.contains(&"data"), "section 'data' missing");
763        // text abs_start = 0x1000, off = 0x00, file_offset = 0x00
764        let text = sections.iter().find(|s| s["name"] == "text").unwrap();
765        assert_eq!(text["address"], "0x0000000000001000");
766        assert_eq!(text["size"], 0x40u64);
767    }
768
769    #[test]
770    fn json_labels_contain_names_and_addresses() {
771        let out = format_json(&make_map());
772        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
773        let labels = v["labels"].as_array().unwrap();
774        let start = labels.iter().find(|l| l["name"] == "start").unwrap();
775        assert_eq!(start["address"], "0x0000000000001000");
776        let end_marker = labels.iter().find(|l| l["name"] == "end_marker").unwrap();
777        assert_eq!(end_marker["address"], "0x000000000000107f");
778    }
779
780    #[test]
781    fn json_consts_contain_names_and_values() {
782        let out = format_json(&make_map());
783        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
784        let consts = v["constants"].as_array().unwrap();
785        let base = consts.iter().find(|c| c["name"] == "BASE").unwrap();
786        assert_eq!(base["value"], "0x0000000000001000");
787        assert_eq!(base["used"], true);
788        let count = consts.iter().find(|c| c["name"] == "COUNT").unwrap();
789        assert_eq!(count["value"], "42");
790        assert_eq!(count["used"], true);
791        let offset = consts.iter().find(|c| c["name"] == "OFFSET").unwrap();
792        assert_eq!(offset["value"], "-10");
793        let version = consts.iter().find(|c| c["name"] == "VERSION").unwrap();
794        assert_eq!(version["value"], "\"v1.0\"");
795    }
796
797    #[test]
798    fn json_empty_tables_are_empty_arrays() {
799        let map = MapDb {
800            output_file: "x.bin".to_string(),
801            base_addr: 0,
802            total_size: 0,
803            sections: vec![],
804            labels: vec![],
805            consts: vec![],
806        };
807        let out = format_json(&map);
808        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
809        assert_eq!(v["sections"].as_array().unwrap().len(), 0);
810        assert_eq!(v["labels"].as_array().unwrap().len(), 0);
811        assert_eq!(v["constants"].as_array().unwrap().len(), 0);
812    }
813
814    #[test]
815    fn json_repeated_section_produces_multiple_entries() {
816        let map = MapDb {
817            output_file: "y.bin".to_string(),
818            base_addr: 0,
819            total_size: 0x20,
820            sections: vec![
821                SectionEntry {
822                    name: "foo".to_string(),
823                    file_offset: 0x00,
824                    off: 0x00,
825                    abs_start: 0x00,
826                    size: 0x10,
827                },
828                SectionEntry {
829                    name: "foo".to_string(),
830                    file_offset: 0x10,
831                    off: 0x10,
832                    abs_start: 0x10,
833                    size: 0x10,
834                },
835            ],
836            labels: vec![],
837            consts: vec![],
838        };
839        let out = format_json(&map);
840        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
841        assert_eq!(v["sections"].as_array().unwrap().len(), 2);
842    }
843
844    // -- format_rs tests -------------------------------------------------------
845
846    #[test]
847    fn rs_is_valid_and_contains_output_file() {
848        let out = format_rs(&make_map());
849        assert!(out.contains("pub mod out_map {"));
850        assert!(out.contains("pub const BASE_ADDR: u64 = 0x0000000000001000;"));
851    }
852
853    #[test]
854    fn rs_sections_contain_names_and_addresses() {
855        let out = format_rs(&make_map());
856        assert!(out.contains("pub const TEXT_ADDR: u64 = 0x0000000000001000;"));
857        assert!(out.contains("pub const TEXT_SIZE: u64 = 64;"));
858        assert!(out.contains("pub const DATA_ADDR: u64 = 0x0000000000001040;"));
859        assert!(out.contains("pub const DATA_SIZE: u64 = 64;"));
860    }
861
862    #[test]
863    fn rs_labels_contain_names_and_addresses() {
864        let out = format_rs(&make_map());
865        assert!(out.contains("pub const START_ADDR: u64 = 0x0000000000001000;"));
866        assert!(out.contains("pub const END_MARKER_ADDR: u64 = 0x000000000000107f;"));
867    }
868
869    #[test]
870    fn rs_consts_contain_names_and_values() {
871        let out = format_rs(&make_map());
872        assert!(out.contains("pub const BASE: u64 = 0x0000000000001000;"));
873        assert!(out.contains("pub const COUNT: i64 = 42;"));
874        assert!(out.contains("pub const OFFSET: i64 = -10;"));
875        assert!(out.contains("pub const VERSION: &str = \"v1.0\";"));
876    }
877}