1use super::html_template::{generate_dashboard_javascript, get_binary_dashboard_template};
5use crate::core::types::{AllocationInfo, MemoryStats};
6use crate::export::binary::error::BinaryExportError;
7use crate::export::binary::reader::BinaryReader;
8use chrono;
9use serde_json::json;
10use std::{fs, path::Path};
11
12pub fn convert_binary_to_html<P: AsRef<Path>>(
14 binary_path: P,
15 html_path: P,
16 project_name: &str,
17) -> Result<(), BinaryExportError> {
18 let mut reader = BinaryReader::new(&binary_path)?;
20 let allocations = reader.read_all()?;
21
22 let stats = generate_statistics(&allocations);
24
25 tracing::debug!("Loading binary dashboard template...");
27 let template = load_binary_dashboard_template()?;
28 tracing::debug!("Template loaded, length: {} chars", template.len());
29
30 let html_content = generate_html_content(&template, &allocations, &stats, project_name)?;
32
33 fs::write(&html_path, html_content).map_err(BinaryExportError::Io)?;
35
36 Ok(())
37}
38
39fn generate_statistics(allocations: &[AllocationInfo]) -> MemoryStats {
41 let mut stats = MemoryStats::new();
42
43 let mut total_memory = 0;
44 let mut active_memory = 0;
45 let mut active_count = 0;
46 let mut leaked_count = 0;
47 let mut leaked_memory = 0;
48
49 for allocation in allocations {
50 stats.total_allocations += 1;
51 total_memory += allocation.size;
52
53 if allocation.timestamp_dealloc.is_none() {
54 active_count += 1;
55 active_memory += allocation.size;
56 }
57
58 if allocation.is_leaked {
59 leaked_count += 1;
60 leaked_memory += allocation.size;
61 }
62 }
63
64 stats.total_allocated = total_memory;
65 stats.active_allocations = active_count;
66 stats.active_memory = active_memory;
67 stats.peak_memory = active_memory; stats.leaked_allocations = leaked_count;
69 stats.leaked_memory = leaked_memory;
70 stats.allocations = allocations.to_vec();
71
72 stats
73}
74
75fn load_binary_dashboard_template() -> Result<String, BinaryExportError> {
77 tracing::debug!("Using embedded binary_dashboard.html template");
79 Ok(get_binary_dashboard_template().to_string())
80}
81
82fn generate_html_content(
84 template: &str,
85 allocations: &[AllocationInfo],
86 stats: &MemoryStats,
87 project_name: &str,
88) -> Result<String, BinaryExportError> {
89 let allocation_data = prepare_allocation_data(allocations)?;
91 let _stats_data = prepare_stats_data(stats)?;
92 let safety_risk_data = prepare_safety_risk_data(allocations)?;
93
94 tracing::debug!(
96 "Replacing BINARY_DATA placeholder with {} bytes of allocation data",
97 allocation_data.len()
98 );
99 let mut html = template.to_string();
100
101 if html.contains("{{PROJECT_NAME}}") {
103 html = html.replace("{{PROJECT_NAME}}", project_name);
104 } else {
105 if let Some(start) = html.find("<title>") {
108 if let Some(end) = html[start..].find("</title>") {
109 let title_end = start + end;
110 let before = &html[..start + 7]; let after = &html[title_end..];
112 html = format!("{before}{project_name} - Memory Analysis Dashboard{after}",);
113 }
114 }
115
116 html = html.replace(
118 "MemScope Memory Analysis Dashboard",
119 &format!("{project_name} - Memory Analysis Report"),
120 );
121
122 html = html.replace("class=\"grid grid-4\"", "class=\"grid grid-4 stats-grid\"");
124 html = html.replace("<table>", "<table class=\"allocations-table\">");
125 }
126
127 html = html.replace(
128 "{{TIMESTAMP}}",
129 &chrono::Utc::now()
130 .format("%Y-%m-%d %H:%M:%S UTC")
131 .to_string(),
132 );
133 html = html.replace(
134 "{{GENERATION_TIME}}",
135 &chrono::Utc::now()
136 .format("%Y-%m-%d %H:%M:%S UTC")
137 .to_string(),
138 );
139
140 if html.contains("{{BINARY_DATA}}") {
142 html = html.replace("{{BINARY_DATA}}", &allocation_data);
143 tracing::debug!("Successfully replaced {{BINARY_DATA}} placeholder with binary data");
144 } else {
145 if let Some(start) = html.find("window.analysisData = {") {
147 if let Some(end) = html[start..].find("};") {
148 let end_pos = start + end + 2; let before = &html[..start];
150 let after = &html[end_pos..];
151 html = format!(
152 "{}window.analysisData = {};{}",
153 before, &allocation_data, after
154 );
155 tracing::debug!(
156 "Fallback: replaced hardcoded window.analysisData with binary data"
157 );
158 }
159 } else {
160 html = html.replace("{{ALLOCATION_DATA}}", &allocation_data);
162 html = html.replace("{{ json_data }}", &allocation_data);
163 html = html.replace("{{json_data}}", &allocation_data);
164 tracing::debug!("Used fallback placeholder replacements");
165 }
166 }
167
168 html = html.replace(
170 "{{TOTAL_ALLOCATIONS}}",
171 &stats.total_allocations.to_string(),
172 );
173 html = html.replace(
174 "{{ACTIVE_ALLOCATIONS}}",
175 &stats.active_allocations.to_string(),
176 );
177 html = html.replace(
178 "{{ACTIVE_MEMORY}}",
179 &format_memory_size(stats.active_memory),
180 );
181 html = html.replace("{{PEAK_MEMORY}}", &format_memory_size(stats.peak_memory));
182 html = html.replace(
183 "{{LEAKED_ALLOCATIONS}}",
184 &stats.leaked_allocations.to_string(),
185 );
186 html = html.replace(
187 "{{LEAKED_MEMORY}}",
188 &format_memory_size(stats.leaked_memory),
189 );
190
191 html = html.replace("{{SVG_IMAGES}}", "<!-- SVG images placeholder -->");
193 html = html.replace("{{CSS_CONTENT}}", "/* Additional CSS placeholder */");
194 html = html.replace("{{JS_CONTENT}}", &generate_dashboard_javascript());
195
196 html = html.replace("{{ json_data }}", &allocation_data);
198 html = html.replace("{{json_data}}", &allocation_data);
199
200 html = html.replace(
202 "AFTER JS_CONTENT loads",
203 "after additional JavaScript loads",
204 );
205 html = html.replace("JS_CONTENT loads", "additional JavaScript loads");
206 html = html.replace("JS_CONTENT", "additionalJavaScript");
207
208 if let Some(dom_ready_start) =
211 html.find("document.addEventListener('DOMContentLoaded', function() {")
212 {
213 let injection_point = dom_ready_start;
214 let before = &html[..injection_point];
215 let after = &html[injection_point..];
216
217 let safety_injection = format!(
218 r#"
219 // Safety Risk Data Injection
220 window.safetyRisks = {safety_risk_data};
221
222 function loadSafetyRisks() {{
223 console.log('🛡️ Loading safety risk data...');
224 const unsafeTable = document.getElementById('unsafeTable');
225 if (!unsafeTable) {{
226 console.warn('⚠️ unsafeTable not found');
227 return;
228 }}
229
230 const risks = window.safetyRisks || [];
231 if (risks.length === 0) {{
232 unsafeTable.innerHTML = '<tr><td colspan="3" class="text-center text-gray-500">No safety risks detected</td></tr>';
233 return;
234 }}
235
236 unsafeTable.innerHTML = '';
237 risks.forEach((risk, index) => {{
238 const row = document.createElement('tr');
239 row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700';
240
241 const riskLevelClass = risk.risk_level === 'High' ? 'text-red-600 font-bold' :
242 risk.risk_level === 'Medium' ? 'text-yellow-600 font-semibold' :
243 'text-green-600';
244
245 row.innerHTML = `
246 <td class="px-3 py-2 text-sm">${{risk.location || 'Unknown'}}</td>
247 <td class="px-3 py-2 text-sm">${{risk.operation || 'Unknown'}}</td>
248 <td class="px-3 py-2 text-sm"><span class="${{riskLevelClass}}">${{risk.risk_level || 'Low'}}</span></td>
249 `;
250 unsafeTable.appendChild(row);
251 }});
252
253 console.log('✅ Safety risks loaded:', risks.length, 'items');
254 }}
255
256 "#,
257 );
258
259 html = format!("{before}{safety_injection}{after}");
260 } else {
261 tracing::debug!("Could not find DOMContentLoaded event listener for safety risk injection");
262 }
263
264 if let Some(manual_init_start) =
266 html.find("manualBtn.addEventListener('click', manualInitialize);")
267 {
268 let after_manual_init =
269 manual_init_start + "manualBtn.addEventListener('click', manualInitialize);".len();
270 let before = &html[..after_manual_init];
271 let after = &html[after_manual_init..];
272
273 let safety_call_injection = r#"
274
275 // Load safety risks after manual initialization
276 setTimeout(function() {
277 loadSafetyRisks();
278 }, 100);
279"#;
280
281 html = format!("{before}{safety_call_injection}{after}");
282 }
283
284 html = html.replace(
286 "console.log('✅ Enhanced dashboard initialized');",
287 "console.log('✅ Enhanced dashboard initialized'); loadSafetyRisks();",
288 );
289
290 tracing::debug!(
291 "Data injection completed: {} allocations, {} stats, safety risks injected",
292 allocations.len(),
293 stats.total_allocations
294 );
295
296 Ok(html)
297}
298
299fn prepare_allocation_data(allocations: &[AllocationInfo]) -> Result<String, BinaryExportError> {
301 let mut allocation_data = Vec::new();
302
303 for allocation in allocations {
304 let mut item = json!({
305 "ptr": format!("0x{:x}", allocation.ptr),
306 "size": allocation.size,
307 "var_name": allocation.var_name.as_deref().unwrap_or("unknown"),
308 "type_name": allocation.type_name.as_deref().unwrap_or("unknown"),
309 "scope_name": allocation.scope_name.as_deref().unwrap_or("global"),
310 "thread_id": allocation.thread_id,
311 "timestamp_alloc": allocation.timestamp_alloc,
312 "timestamp_dealloc": allocation.timestamp_dealloc,
313 "is_leaked": allocation.is_leaked,
314 "lifetime_ms": allocation.lifetime_ms,
315 "borrow_count": allocation.borrow_count,
316 });
317
318 if let Some(ref borrow_info) = allocation.borrow_info {
320 item["borrow_info"] = json!({
321 "immutable_borrows": borrow_info.immutable_borrows,
322 "mutable_borrows": borrow_info.mutable_borrows,
323 "max_concurrent_borrows": borrow_info.max_concurrent_borrows,
324 "last_borrow_timestamp": borrow_info.last_borrow_timestamp,
325 });
326 }
327
328 if let Some(ref clone_info) = allocation.clone_info {
329 item["clone_info"] = json!({
330 "clone_count": clone_info.clone_count,
331 "is_clone": clone_info.is_clone,
332 "original_ptr": clone_info.original_ptr.map(|p| format!("0x{p:x}")),
333 });
334 }
335
336 item["ownership_history_available"] = json!(allocation.ownership_history_available);
337
338 allocation_data.push(item);
339 }
340
341 let (lifetime_data, complex_types, unsafe_ffi, performance_data) =
343 generate_enhanced_data(&allocation_data);
344
345 let data_structure = json!({
347 "memory_analysis": {
348 "allocations": allocation_data.clone()
349 },
350 "allocations": allocation_data, "lifetime": lifetime_data,
352 "complex_types": complex_types,
353 "unsafe_ffi": unsafe_ffi,
354 "performance": performance_data,
355 "metadata": {
356 "generation_time": chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(),
357 "data_source": "binary_direct",
358 "version": "1.0"
359 }
360 });
361
362 serde_json::to_string(&data_structure).map_err(|e| {
363 BinaryExportError::SerializationError(format!("Failed to serialize allocation data: {e}"))
364 })
365}
366
367fn prepare_stats_data(stats: &MemoryStats) -> Result<String, BinaryExportError> {
369 let data = json!({
370 "total_allocations": stats.total_allocations,
371 "total_allocated": stats.total_allocated,
372 "active_allocations": stats.active_allocations,
373 "active_memory": stats.active_memory,
374 "peak_allocations": stats.peak_allocations,
375 "peak_memory": stats.peak_memory,
376 "total_deallocations": stats.total_deallocations,
377 "total_deallocated": stats.total_deallocated,
378 "leaked_allocations": stats.leaked_allocations,
379 "leaked_memory": stats.leaked_memory,
380 });
381
382 serde_json::to_string(&data).map_err(|e| {
383 BinaryExportError::SerializationError(format!("Failed to serialize stats data: {e}"))
384 })
385}
386
387fn format_memory_size(bytes: usize) -> String {
389 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
390 let mut size = bytes as f64;
391 let mut unit_index = 0;
392
393 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
394 size /= 1024.0;
395 unit_index += 1;
396 }
397
398 if unit_index == 0 {
399 format!("{bytes} {}", UNITS[unit_index])
400 } else {
401 format!("{size:.2} {}", UNITS[unit_index])
402 }
403}
404
405fn generate_enhanced_data(
407 allocations: &[serde_json::Value],
408) -> (
409 serde_json::Value,
410 serde_json::Value,
411 serde_json::Value,
412 serde_json::Value,
413) {
414 let lifetime_allocations: Vec<serde_json::Value> = allocations
416 .iter()
417 .map(|alloc| {
418 let mut lifetime_alloc = alloc.clone();
419 lifetime_alloc["ownership_transfer_points"] =
420 json!(generate_ownership_transfer_points(alloc));
421 lifetime_alloc
422 })
423 .collect();
424
425 let lifetime_data = json!({
426 "allocations": lifetime_allocations,
427 "metadata": {
428 "generation_time": chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(),
429 "data_source": "binary_direct"
430 }
431 });
432
433 let complex_allocations: Vec<serde_json::Value> = allocations
435 .iter()
436 .filter_map(|alloc| {
437 let type_name = alloc["type_name"].as_str().unwrap_or("");
438 if type_name.contains('<')
440 || type_name.contains("Arc")
441 || type_name.contains("Box")
442 || type_name.contains("Vec")
443 || type_name.contains("HashMap")
444 || type_name.contains("BTreeMap")
445 || type_name.contains("Rc")
446 || type_name.contains("RefCell")
447 {
448 let mut complex_alloc = alloc.clone();
449 complex_alloc["generic_params"] = json!(extract_generic_params(type_name));
450 complex_alloc["complexity_score"] = json!(calculate_complexity_score(type_name));
451 complex_alloc["memory_layout"] = json!({
452 "alignment": 8,
453 "padding": 0,
454 "size_bytes": alloc["size"]
455 });
456 Some(complex_alloc)
457 } else {
458 None
459 }
460 })
461 .collect();
462
463 let complex_types = json!({
464 "allocations": complex_allocations,
465 "metadata": {
466 "generation_time": chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(),
467 "data_source": "binary_direct"
468 }
469 });
470
471 let unsafe_allocations: Vec<serde_json::Value> = allocations
473 .iter()
474 .map(|alloc| {
475 let type_name = alloc["type_name"].as_str().unwrap_or("");
476 let is_ffi_tracked = type_name.contains("*mut")
477 || type_name.contains("*const")
478 || type_name.contains("c_void")
479 || type_name.contains("CString")
480 || type_name.contains("extern")
481 || type_name.contains("CStr");
482
483 let safety_violations: Vec<&str> = if is_ffi_tracked {
484 vec!["raw_pointer_usage", "ffi_boundary_crossing"]
485 } else if alloc["is_leaked"].as_bool().unwrap_or(false) {
486 vec!["memory_leak"]
487 } else {
488 vec![]
489 };
490
491 let mut unsafe_alloc = alloc.clone();
492 unsafe_alloc["ffi_tracked"] = json!(is_ffi_tracked);
493 unsafe_alloc["safety_violations"] = json!(safety_violations);
494 unsafe_alloc
495 })
496 .collect();
497
498 let unsafe_ffi = json!({
499 "allocations": unsafe_allocations,
500 "metadata": {
501 "generation_time": chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(),
502 "data_source": "binary_direct"
503 }
504 });
505
506 let performance_allocations: Vec<serde_json::Value> = allocations
508 .iter()
509 .map(|alloc| {
510 let size = alloc["size"].as_u64().unwrap_or(0);
511 let lifetime_ms = alloc["lifetime_ms"].as_u64().unwrap_or(0);
512
513 let mut perf_alloc = alloc.clone();
514 perf_alloc["fragmentation_analysis"] = json!({
515 "fragmentation_score": if size > 1024 { 0.3 } else { 0.1 },
516 "alignment_efficiency": if size % 8 == 0 { 100.0 } else { 85.0 },
517 "memory_density": calculate_memory_density(size)
518 });
519 perf_alloc["allocation_efficiency"] = json!({
520 "reuse_potential": if lifetime_ms > 1000 { 0.2 } else { 0.8 },
521 "memory_locality": if size < 1024 { "high" } else { "medium" },
522 "cache_efficiency": calculate_cache_efficiency(size)
523 });
524 perf_alloc
525 })
526 .collect();
527
528 let performance_data = json!({
529 "allocations": performance_allocations,
530 "metadata": {
531 "generation_time": chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(),
532 "data_source": "binary_direct"
533 }
534 });
535
536 (lifetime_data, complex_types, unsafe_ffi, performance_data)
537}
538
539fn extract_generic_params(type_name: &str) -> Vec<String> {
541 if let Some(start) = type_name.find('<') {
542 if let Some(end) = type_name.rfind('>') {
543 let params_str = &type_name[start + 1..end];
544 return params_str
545 .split(',')
546 .map(|s| s.trim().to_string())
547 .collect();
548 }
549 }
550 vec![]
551}
552
553fn calculate_complexity_score(type_name: &str) -> u32 {
555 let mut score = 1;
556
557 score += type_name.matches('<').count() as u32 * 2;
559
560 if type_name.contains("Arc") || type_name.contains("Rc") {
562 score += 3;
563 }
564 if type_name.contains("Box") {
565 score += 2;
566 }
567 if type_name.contains("Vec") {
568 score += 2;
569 }
570 if type_name.contains("HashMap") || type_name.contains("BTreeMap") {
571 score += 4;
572 }
573
574 if type_name.contains("*mut") || type_name.contains("*const") {
576 score += 5;
577 }
578
579 score
580}
581
582fn calculate_memory_density(size: u64) -> f64 {
584 if size < 64 {
586 1.0
587 } else if size < 1024 {
588 0.8
589 } else if size < 4096 {
590 0.6
591 } else {
592 0.4
593 }
594}
595
596fn calculate_cache_efficiency(size: u64) -> f64 {
598 let cache_line_size = 64;
600 let lines_used = size.div_ceil(cache_line_size);
601 let efficiency = size as f64 / (lines_used * cache_line_size) as f64;
602 efficiency.min(1.0)
603}
604
605fn generate_ownership_transfer_points(allocation: &serde_json::Value) -> Vec<serde_json::Value> {
607 let mut transfer_points = Vec::new();
608
609 if let Some(clone_info) = allocation.get("clone_info") {
611 if clone_info
612 .get("is_clone")
613 .and_then(|v| v.as_bool())
614 .unwrap_or(false)
615 {
616 transfer_points.push(json!({
617 "event": "clone_created",
618 "timestamp": allocation.get("timestamp_alloc"),
619 "original_ptr": clone_info.get("original_ptr")
620 }));
621 }
622
623 let clone_count = clone_info
624 .get("clone_count")
625 .and_then(|v| v.as_u64())
626 .unwrap_or(0);
627 if clone_count > 0 {
628 transfer_points.push(json!({
629 "event": "clones_created",
630 "count": clone_count,
631 "timestamp": allocation.get("timestamp_alloc")
632 }));
633 }
634 }
635
636 if let Some(borrow_info) = allocation.get("borrow_info") {
638 if let Some(last_borrow) = borrow_info.get("last_borrow_timestamp") {
639 transfer_points.push(json!({
640 "event": "last_borrow",
641 "timestamp": last_borrow,
642 "borrow_type": "mixed"
643 }));
644 }
645 }
646
647 transfer_points
648}
649
650fn prepare_safety_risk_data(allocations: &[AllocationInfo]) -> Result<String, BinaryExportError> {
652 let mut safety_risks = Vec::new();
653
654 for allocation in allocations {
656 if allocation.size > 1024 * 1024 {
660 safety_risks.push(json!({
662 "location": format!("{}::{}",
663 allocation.scope_name.as_deref().unwrap_or("unknown"),
664 allocation.var_name.as_deref().unwrap_or("unnamed")),
665 "operation": "Large Memory Allocation",
666 "risk_level": "Medium",
667 "description": format!("Large allocation of {} bytes may indicate unsafe buffer operations", allocation.size)
668 }));
669 }
670
671 if allocation.is_leaked {
673 safety_risks.push(json!({
674 "location": format!("{}::{}",
675 allocation.scope_name.as_deref().unwrap_or("unknown"),
676 allocation.var_name.as_deref().unwrap_or("unnamed")),
677 "operation": "Memory Leak",
678 "risk_level": "High",
679 "description": "Memory leak detected - potential unsafe memory management"
680 }));
681 }
682
683 if allocation.borrow_count > 10 {
685 safety_risks.push(json!({
686 "location": format!("{}::{}",
687 allocation.scope_name.as_deref().unwrap_or("unknown"),
688 allocation.var_name.as_deref().unwrap_or("unnamed")),
689 "operation": "High Borrow Count",
690 "risk_level": "Medium",
691 "description": format!("High borrow count ({}) may indicate unsafe sharing patterns", allocation.borrow_count)
692 }));
693 }
694
695 if let Some(type_name) = &allocation.type_name {
697 if type_name.contains("*mut") || type_name.contains("*const") {
698 safety_risks.push(json!({
699 "location": format!("{}::{}",
700 allocation.scope_name.as_deref().unwrap_or("unknown"),
701 allocation.var_name.as_deref().unwrap_or("unnamed")),
702 "operation": "Raw Pointer Usage",
703 "risk_level": "High",
704 "description": format!("Raw pointer type '{}' requires unsafe operations", type_name)
705 }));
706 }
707
708 if type_name.contains("CString")
710 || type_name.contains("CStr")
711 || type_name.contains("c_void")
712 || type_name.contains("extern")
713 {
714 safety_risks.push(json!({
715 "location": format!("{}::{}",
716 allocation.scope_name.as_deref().unwrap_or("unknown"),
717 allocation.var_name.as_deref().unwrap_or("unnamed")),
718 "operation": "FFI Boundary Crossing",
719 "risk_level": "Medium",
720 "description": format!("FFI type '{}' crosses safety boundaries", type_name)
721 }));
722 }
723 }
724
725 if let Some(lifetime_ms) = allocation.lifetime_ms {
727 if lifetime_ms < 1 {
728 safety_risks.push(json!({
730 "location": format!("{}::{}",
731 allocation.scope_name.as_deref().unwrap_or("unknown"),
732 allocation.var_name.as_deref().unwrap_or("unnamed")),
733 "operation": "Short-lived Allocation",
734 "risk_level": "Low",
735 "description": format!("Very short lifetime ({}ms) may indicate unsafe temporary operations", lifetime_ms)
736 }));
737 }
738 }
739 }
740
741 if safety_risks.is_empty() {
743 safety_risks.push(json!({
744 "location": "Global Analysis",
745 "operation": "Safety Scan Complete",
746 "risk_level": "Low",
747 "description": "No significant safety risks detected in current allocations"
748 }));
749 }
750
751 serde_json::to_string(&safety_risks).map_err(|e| {
752 BinaryExportError::SerializationError(format!("Failed to serialize safety risk data: {e}",))
753 })
754}
755
756pub fn parse_binary_to_html_direct<P: AsRef<Path>>(
758 binary_path: P,
759 html_path: P,
760 project_name: &str,
761) -> Result<(), BinaryExportError> {
762 convert_binary_to_html(binary_path, html_path, project_name)
763}