1use serde::Serialize;
2
3use crate::elf::{ElfAnalysis, FirmwareUsage, RegionType, SectionInfo};
4
5pub 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
17pub 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
28pub fn format_report(usage: &FirmwareUsage, bar_width: usize) -> String {
30 format_report_with_labels(usage, bar_width, "FLASH", "RAM")
31}
32
33pub 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
90pub 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 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
140pub 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#[derive(Debug, Clone, Serialize)]
155pub struct ExportMeta {
156 pub tool: String,
157 pub version: String,
158 pub repository: String,
159}
160
161#[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#[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}