Skip to main content

st_mem/
display.rs

1use serde::Serialize;
2
3use crate::elf::{ElfAnalysis, FirmwareUsage, RegionType, SectionInfo};
4
5/// Generate a text-based progress bar.
6pub fn progress_bar(pct: f64, width: usize) -> String {
7    let fill = if pct <= 0.0 {
8        0
9    } else {
10        let f = (pct / 100.0 * width as f64) as usize;
11        if f < 1 { 1 } else { f }.min(width)
12    };
13    let empty = width - fill;
14    format!("[{}{}]", "\u{2588}".repeat(fill), "\u{2591}".repeat(empty))
15}
16
17/// Format byte count into a human-readable string (B, KB, MB).
18pub fn format_bytes(bytes: u64) -> String {
19    if bytes >= 1024 * 1024 {
20        format!("{} MB", bytes / (1024 * 1024))
21    } else if bytes >= 1024 {
22        format!("{} KB", bytes / 1024)
23    } else {
24        format!("{} B", bytes)
25    }
26}
27
28/// Generate a full firmware memory report as a formatted string.
29pub fn format_report(usage: &FirmwareUsage, bar_width: usize) -> String {
30    format_report_with_labels(usage, bar_width, "FLASH", "RAM")
31}
32
33/// Generate a firmware memory report with custom region labels.
34pub fn format_report_with_labels(
35    usage: &FirmwareUsage,
36    bar_width: usize,
37    flash_label: &str,
38    ram_label: &str,
39) -> String {
40    let flash_bar = progress_bar(usage.flash_percent(), bar_width);
41    let ram_bar = progress_bar(usage.ram_percent(), bar_width);
42
43    let flash_used_str = format_bytes(usage.flash_used);
44    let flash_total_str = format_bytes(usage.flash_total);
45    let ram_used_str = format_bytes(usage.ram_used);
46    let ram_total_str = format_bytes(usage.ram_total);
47
48    let label_width = flash_label.len().max(ram_label.len());
49
50    let line_flash = format!(
51        " {:<label$} {} {:>5.1}%  {:>6} / {:<6} ",
52        flash_label,
53        flash_bar,
54        usage.flash_percent(),
55        flash_used_str,
56        flash_total_str,
57        label = label_width
58    );
59    let line_ram = format!(
60        " {:<label$} {} {:>5.1}%  {:>6} / {:<6} ",
61        ram_label,
62        ram_bar,
63        usage.ram_percent(),
64        ram_used_str,
65        ram_total_str,
66        label = label_width
67    );
68
69    let total_width = line_flash.chars().count().max(line_ram.chars().count());
70    let border = format!("+{}+", "-".repeat(total_width));
71
72    let pad = |s: &str, w: usize| {
73        let chars: Vec<char> = s.chars().collect();
74        if chars.len() >= w {
75            chars[..w].iter().collect()
76        } else {
77            format!("{}{}", s, " ".repeat(w - chars.len()))
78        }
79    };
80
81    let mut out = String::new();
82    out.push_str("  [FIRMWARE SIZE]\n");
83    out.push_str(&format!("  {}\n", border));
84    out.push_str(&format!("  |{}|\n", pad(&line_flash, total_width)));
85    out.push_str(&format!("  |{}|\n", pad(&line_ram, total_width)));
86    out.push_str(&format!("  {}", border));
87    out
88}
89
90/// Format a per-section breakdown table.
91///
92/// Shows each allocated section with its name, region type, size,
93/// percentage of region, and a small progress bar.
94pub fn format_sections(analysis: &ElfAnalysis, bar_width: usize) -> String {
95    let mut out = String::new();
96
97    let flash_sections: Vec<&SectionInfo> = analysis.sections.iter()
98        .filter(|s| s.region == RegionType::Flash)
99        .collect();
100    let ram_sections: Vec<&SectionInfo> = analysis.sections.iter()
101        .filter(|s| s.region == RegionType::Ram)
102        .collect();
103
104    // Find the longest section name for alignment
105    let max_name = analysis.sections.iter()
106        .map(|s| s.name.len())
107        .max()
108        .unwrap_or(8)
109        .max(8);
110
111    if !flash_sections.is_empty() {
112        out.push_str(&format!("\n  [FLASH Sections] ({} total)\n", format_bytes(analysis.usage.flash_used)));
113        out.push_str(&format!("  {:<name$}  {:>8}  {:>6}  {}\n",
114            "NAME", "SIZE", "%", "BAR", name = max_name));
115        for sec in &flash_sections {
116            let pct = if analysis.usage.flash_total == 0 { 0.0 }
117                else { sec.size as f64 * 100.0 / analysis.usage.flash_total as f64 };
118            let bar = progress_bar(pct, bar_width);
119            out.push_str(&format!("  {:<name$}  {:>8}  {:>5.1}%  {}\n",
120                sec.name, format_bytes(sec.size as u64), pct, bar, name = max_name));
121        }
122    }
123
124    if !ram_sections.is_empty() {
125        out.push_str(&format!("\n  [RAM Sections] ({} total)\n", format_bytes(analysis.usage.ram_used)));
126        out.push_str(&format!("  {:<name$}  {:>8}  {:>6}  {}\n",
127            "NAME", "SIZE", "%", "BAR", name = max_name));
128        for sec in &ram_sections {
129            let pct = if analysis.usage.ram_total == 0 { 0.0 }
130                else { sec.size as f64 * 100.0 / analysis.usage.ram_total as f64 };
131            let bar = progress_bar(pct, bar_width);
132            out.push_str(&format!("  {:<name$}  {:>8}  {:>5.1}%  {}\n",
133                sec.name, format_bytes(sec.size as u64), pct, bar, name = max_name));
134        }
135    }
136
137    out
138}
139
140/// Convenience: analyze and print a firmware memory report to stdout.
141pub fn print_report<P1: AsRef<std::path::Path>, P2: AsRef<std::path::Path>>(
142    elf_path: P1,
143    memory_x_path: P2,
144) -> Result<(), String> {
145    let config = crate::memory::MemoryConfig::from_file(memory_x_path)?;
146    let usage = crate::elf::analyze_elf(elf_path, &config)?;
147    println!("{}", format_report(&usage, 30));
148    Ok(())
149}
150
151// ── Export structures ──────────────────────────────────────────────────────
152
153/// Export-friendly metadata.
154#[derive(Debug, Clone, Serialize)]
155pub struct ExportMeta {
156    pub tool: String,
157    pub version: String,
158    pub repository: String,
159}
160
161/// Full report structure for JSON / Markdown export.
162#[derive(Debug, Clone, Serialize)]
163pub struct ExportReport {
164    pub meta: ExportMeta,
165    pub flash_used: u64,
166    pub flash_total: u64,
167    pub flash_percent: f64,
168    pub ram_used: u64,
169    pub ram_total: u64,
170    pub ram_percent: f64,
171    pub sections: Vec<ExportSection>,
172}
173
174/// A single section entry for export.
175#[derive(Debug, Clone, Serialize)]
176pub struct ExportSection {
177    pub name: String,
178    pub address: String,
179    pub size: u32,
180    pub region: String,
181    pub flags: Vec<String>,
182}
183
184impl ExportReport {
185    pub fn from_analysis(analysis: &ElfAnalysis) -> Self {
186        let sections = analysis.sections.iter().map(|s| {
187            let mut flags = Vec::new();
188            if s.is_alloc() { flags.push("ALLOC".to_string()); }
189            if s.is_writable() { flags.push("WRITE".to_string()); }
190            if s.is_executable() { flags.push("EXEC".to_string()); }
191            ExportSection {
192                name: s.name.clone(),
193                address: format!("0x{:08X}", s.address),
194                size: s.size,
195                region: match s.region {
196                    RegionType::Flash => "FLASH",
197                    RegionType::Ram => "RAM",
198                    RegionType::Other => "OTHER",
199                }.to_string(),
200                flags,
201            }
202        }).collect();
203
204        ExportReport {
205            meta: ExportMeta {
206                tool: "st-mem".to_string(),
207                version: env!("CARGO_PKG_VERSION").to_string(),
208                repository: env!("CARGO_PKG_REPOSITORY").to_string(),
209            },
210            flash_used: analysis.usage.flash_used,
211            flash_total: analysis.usage.flash_total,
212            flash_percent: analysis.usage.flash_percent(),
213            ram_used: analysis.usage.ram_used,
214            ram_total: analysis.usage.ram_total,
215            ram_percent: analysis.usage.ram_percent(),
216            sections,
217        }
218    }
219
220    pub fn to_json(&self) -> String {
221        serde_json::to_string_pretty(self).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
222    }
223
224    pub fn to_markdown(&self) -> String {
225        let mut md = String::new();
226        md.push_str("# st-mem Firmware Report\n\n");
227        md.push_str(&format!("Generated by **{} v{}** — {}\n\n",
228            self.meta.tool, self.meta.version, self.meta.repository));
229
230        md.push_str("## Memory Usage\n\n");
231        md.push_str("| Region | Used | Total | Percent |\n");
232        md.push_str("|--------|------|-------|----------|\n");
233        md.push_str(&format!("| FLASH | {} | {} | {:.1}% |\n",
234            format_bytes(self.flash_used), format_bytes(self.flash_total), self.flash_percent));
235        md.push_str(&format!("| RAM | {} | {} | {:.1}% |\n",
236            format_bytes(self.ram_used), format_bytes(self.ram_total), self.ram_percent));
237
238        if !self.sections.is_empty() {
239            md.push_str("\n## Sections\n\n");
240            md.push_str("| Name | Region | Address | Size | Flags |\n");
241            md.push_str("|------|--------|---------|------|-------|\n");
242            for sec in &self.sections {
243                md.push_str(&format!("| {} | {} | {} | {} | {} |\n",
244                    sec.name, sec.region, sec.address,
245                    format_bytes(sec.size as u64),
246                    sec.flags.join(", ")));
247            }
248        }
249
250        md
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_progress_bar() {
260        assert_eq!(progress_bar(0.0, 10), "[░░░░░░░░░░]");
261        assert_eq!(progress_bar(100.0, 10), "[██████████]");
262        assert_eq!(progress_bar(50.0, 10), "[█████░░░░░]");
263    }
264
265    #[test]
266    fn test_format_bytes() {
267        assert_eq!(format_bytes(500), "500 B");
268        assert_eq!(format_bytes(1024), "1 KB");
269        assert_eq!(format_bytes(65536), "64 KB");
270        assert_eq!(format_bytes(1048576), "1 MB");
271    }
272
273    #[test]
274    fn test_format_report() {
275        let usage = FirmwareUsage {
276            flash_used: 9933,
277            ram_used: 4,
278            flash_total: 65536,
279            ram_total: 20480,
280        };
281        let report = format_report(&usage, 30);
282        assert!(report.contains("FIRMWARE SIZE"));
283        assert!(report.contains("FLASH"));
284        assert!(report.contains("RAM"));
285    }
286
287    #[test]
288    fn test_export_json() {
289        let analysis = crate::elf::analyze_elf_detailed("stm32dome",
290            &crate::memory::MemoryConfig::from_file("memory.x").unwrap()
291        ).unwrap();
292        let report = ExportReport::from_analysis(&analysis);
293        let json = report.to_json();
294        assert!(json.contains("\"flash_used\""));
295        assert!(json.contains("\"sections\""));
296    }
297
298    #[test]
299    fn test_export_markdown() {
300        let analysis = crate::elf::analyze_elf_detailed("stm32dome",
301            &crate::memory::MemoryConfig::from_file("memory.x").unwrap()
302        ).unwrap();
303        let report = ExportReport::from_analysis(&analysis);
304        let md = report.to_markdown();
305        assert!(md.contains("# st-mem Firmware Report"));
306        assert!(md.contains("## Memory Usage"));
307    }
308}