1extern crate alloc;
8
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use core::fmt::Write;
13
14use crate::colors::{bold, bold_cyan, bold_green, bold_red, bold_yellow, dim, green, red, yellow};
15use crate::result::{
16 Diagnostics, EffectEstimate, Exploitability, MeasurementQuality, Outcome, PreflightCategory,
17 PreflightSeverity, ResearchOutcome, ResearchStatus,
18};
19
20pub const SEPARATOR: &str = "──────────────────────────────────────────────────────────────";
22
23const DEFAULT_WRAP_WIDTH: usize = 72;
25
26pub fn format_outcome_plain(outcome: &Outcome) -> String {
34 let mut out = String::new();
35
36 writeln!(out, "tacet").unwrap();
37 writeln!(out, "{}", SEPARATOR).unwrap();
38 writeln!(out).unwrap();
39
40 match outcome {
41 Outcome::Pass {
42 leak_probability,
43 effect,
44 samples_used,
45 quality,
46 diagnostics,
47 ..
48 } => {
49 format_header(&mut out, *samples_used, *quality);
50 writeln!(out, " {}", bold_green("\u{2713} No timing leak detected")).unwrap();
51 writeln!(out).unwrap();
52 format_pass_body(&mut out, *leak_probability, effect);
53 format_preflight_notes(&mut out, diagnostics);
54 format_diagnostics_section(&mut out, diagnostics);
55 format_reproduction_line(&mut out, diagnostics);
56 format_debug_environment(&mut out, diagnostics);
57 }
58
59 Outcome::Fail {
60 leak_probability,
61 effect,
62 exploitability,
63 samples_used,
64 quality,
65 diagnostics,
66 ..
67 } => {
68 format_header(&mut out, *samples_used, *quality);
69 writeln!(out, " {}", bold_yellow("\u{26A0} Timing leak detected")).unwrap();
70 writeln!(out).unwrap();
71 format_fail_body(&mut out, *leak_probability, effect, *exploitability);
72 format_preflight_notes(&mut out, diagnostics);
73 format_diagnostics_section(&mut out, diagnostics);
74 format_reproduction_line(&mut out, diagnostics);
75 format_debug_environment(&mut out, diagnostics);
76 }
77
78 Outcome::Inconclusive {
79 reason,
80 leak_probability,
81 effect,
82 samples_used,
83 quality,
84 diagnostics,
85 ..
86 } => {
87 format_header(&mut out, *samples_used, *quality);
88 writeln!(out, " {}", bold_cyan("? Inconclusive")).unwrap();
89 writeln!(out, " {}", reason).unwrap();
90 writeln!(out).unwrap();
91 format_inconclusive_body(&mut out, *leak_probability, effect);
92 format_inconclusive_diagnostics(&mut out, diagnostics);
93 format_preflight_validation(&mut out, diagnostics);
94 format_diagnostics_section(&mut out, diagnostics);
95 format_reproduction_line(&mut out, diagnostics);
96 format_debug_environment(&mut out, diagnostics);
97 }
98
99 Outcome::Unmeasurable {
100 operation_ns,
101 threshold_ns,
102 platform,
103 recommendation,
104 } => {
105 writeln!(
106 out,
107 " {}",
108 bold_yellow("\u{26A0} Operation too fast to measure reliably")
109 )
110 .unwrap();
111 writeln!(out).unwrap();
112 writeln!(out, " Estimated duration: ~{:.1} ns", operation_ns).unwrap();
113 writeln!(out, " Minimum measurable: ~{:.1} ns", threshold_ns).unwrap();
114 writeln!(out, " Platform: {}", platform).unwrap();
115 writeln!(out).unwrap();
116 writeln!(out, " Recommendation: {}", recommendation).unwrap();
117 writeln!(out).unwrap();
118 writeln!(out, "{}", SEPARATOR).unwrap();
119 write!(
120 out,
121 "Note: Results are unmeasurable at this resolution; no leak probability is reported."
122 )
123 .unwrap();
124 return out;
125 }
126
127 Outcome::Research(research) => {
128 format_research_outcome(&mut out, research);
129 }
130 }
131
132 writeln!(out).unwrap();
133 writeln!(out, "{}", SEPARATOR).unwrap();
134
135 if matches!(outcome, Outcome::Fail { .. }) {
136 write!(
137 out,
138 "Note: Exploitability is a heuristic estimate based on effect magnitude."
139 )
140 .unwrap();
141 }
142
143 out
144}
145
146pub fn format_debug_summary_plain(outcome: &Outcome) -> String {
148 let mut out = String::new();
149
150 writeln!(
151 out,
152 "\u{250C}\u{2500} Debug Summary {}",
153 "\u{2500}".repeat(40)
154 )
155 .unwrap();
156
157 match outcome {
158 Outcome::Pass {
159 leak_probability,
160 effect,
161 quality,
162 samples_used,
163 diagnostics,
164 ..
165 }
166 | Outcome::Fail {
167 leak_probability,
168 effect,
169 quality,
170 samples_used,
171 diagnostics,
172 ..
173 }
174 | Outcome::Inconclusive {
175 leak_probability,
176 effect,
177 quality,
178 samples_used,
179 diagnostics,
180 ..
181 } => {
182 let outcome_type = match outcome {
183 Outcome::Pass { .. } => "PASS",
184 Outcome::Fail { .. } => "FAIL",
185 Outcome::Inconclusive { .. } => "INCONCLUSIVE",
186 _ => unreachable!(),
187 };
188 writeln!(out, "\u{2502} Outcome = {}", outcome_type).unwrap();
189 format_debug_core_metrics(
190 &mut out,
191 *leak_probability,
192 effect,
193 *quality,
194 *samples_used,
195 diagnostics,
196 );
197 format_debug_warnings(&mut out, diagnostics);
198 format_debug_diagnostics(&mut out, diagnostics);
199 }
200
201 Outcome::Unmeasurable {
202 operation_ns,
203 threshold_ns,
204 platform,
205 recommendation,
206 } => {
207 writeln!(out, "\u{2502} Outcome = UNMEASURABLE").unwrap();
208 writeln!(out, "\u{2502} Operation: ~{:.1}ns", operation_ns).unwrap();
209 writeln!(out, "\u{2502} Threshold: ~{:.1}ns", threshold_ns).unwrap();
210 writeln!(out, "\u{2502} Platform: {}", platform).unwrap();
211 writeln!(out, "\u{2502} Tip: {}", recommendation).unwrap();
212 }
213
214 Outcome::Research(research) => {
215 format_debug_research(&mut out, research);
216 }
217 }
218
219 write!(out, "\u{2514}{}", "\u{2500}".repeat(55)).unwrap();
220
221 out
222}
223
224fn format_header(out: &mut String, samples_used: usize, quality: MeasurementQuality) {
229 writeln!(out, " Samples: {} per class", samples_used).unwrap();
230 writeln!(out, " Quality: {}", format_quality_colored(quality)).unwrap();
231 writeln!(out).unwrap();
232}
233
234fn format_quality_colored(quality: MeasurementQuality) -> String {
236 match quality {
237 MeasurementQuality::Excellent => green("Excellent"),
238 MeasurementQuality::Good => green("Good"),
239 MeasurementQuality::Poor => yellow("Poor"),
240 MeasurementQuality::TooNoisy => yellow("too noisy"),
241 }
242}
243
244fn format_pass_body(out: &mut String, leak_probability: f64, effect: &EffectEstimate) {
245 writeln!(
246 out,
247 " Probability of leak: {:.1}%",
248 leak_probability * 100.0
249 )
250 .unwrap();
251 writeln!(
252 out,
253 " 95% CI: {:.1}\u{2013}{:.1} ns",
254 effect.credible_interval_ns.0, effect.credible_interval_ns.1
255 )
256 .unwrap();
257}
258
259fn format_fail_body(
260 out: &mut String,
261 leak_probability: f64,
262 effect: &EffectEstimate,
263 exploitability: Exploitability,
264) {
265 writeln!(
266 out,
267 " Probability of leak: {:.1}%",
268 leak_probability * 100.0
269 )
270 .unwrap();
271
272 writeln!(
273 out,
274 " Max effect: {:.1} ns [CI: {:.1}\u{2013}{:.1}]",
275 effect.max_effect_ns, effect.credible_interval_ns.0, effect.credible_interval_ns.1
276 )
277 .unwrap();
278
279 if !effect.top_quantiles.is_empty() {
281 writeln!(out, " Hotspots:").unwrap();
282 for tq in &effect.top_quantiles {
283 writeln!(
284 out,
285 " \u{2192} p{:.0} ({:.1}ns, {:.0}% confident)",
286 tq.quantile_p * 100.0,
287 tq.mean_ns,
288 tq.exceed_prob * 100.0
289 )
290 .unwrap();
291 }
292 }
293
294 writeln!(out).unwrap();
295 writeln!(out, " Exploitability (heuristic):").unwrap();
296 let (vector, queries) = exploitability_info_colored(exploitability);
297 writeln!(out, " Attack vector: {}", vector).unwrap();
298 writeln!(out, " Queries needed: {}", queries).unwrap();
299}
300
301fn format_inconclusive_body(out: &mut String, leak_probability: f64, effect: &EffectEstimate) {
302 writeln!(
303 out,
304 " Current probability of leak: {:.1}%",
305 leak_probability * 100.0
306 )
307 .unwrap();
308
309 writeln!(
310 out,
311 " Max effect estimate: {:.1} ns [CI: {:.1}\u{2013}{:.1}]",
312 effect.max_effect_ns, effect.credible_interval_ns.0, effect.credible_interval_ns.1
313 )
314 .unwrap();
315}
316
317fn format_research_outcome(out: &mut String, research: &ResearchOutcome) {
318 writeln!(out, " Samples: {} per class", research.samples_used).unwrap();
319 writeln!(
320 out,
321 " Quality: {}",
322 format_quality_colored(research.quality)
323 )
324 .unwrap();
325 writeln!(out).unwrap();
326
327 let status_line = match &research.status {
328 ResearchStatus::EffectDetected => bold_green("\u{1F50D} Effect Detected"),
329 ResearchStatus::NoEffectDetected => bold_cyan("\u{2713} No Effect Detected"),
330 ResearchStatus::ResolutionLimitReached => bold_yellow("\u{26A0} Resolution Limit Reached"),
331 ResearchStatus::QualityIssue(_) => bold_yellow("? Quality Issue"),
332 ResearchStatus::BudgetExhausted => bold_cyan("? Budget Exhausted"),
333 };
334 writeln!(out, " {}", status_line).unwrap();
335 writeln!(out).unwrap();
336
337 writeln!(out, " Max effect: {:.1} ns", research.max_effect_ns).unwrap();
338 writeln!(
339 out,
340 " 95% CI: [{:.1}, {:.1}] ns",
341 research.max_effect_ci.0, research.max_effect_ci.1
342 )
343 .unwrap();
344 writeln!(out, " Measurement floor: {:.1} ns", research.theta_floor).unwrap();
345 writeln!(
346 out,
347 " Detectable: {}",
348 if research.detectable { "yes" } else { "no" }
349 )
350 .unwrap();
351
352 writeln!(out).unwrap();
353 writeln!(
354 out,
355 " Max effect: {:.1} ns [CI: {:.1}\u{2013}{:.1}]",
356 research.effect.max_effect_ns,
357 research.effect.credible_interval_ns.0,
358 research.effect.credible_interval_ns.1
359 )
360 .unwrap();
361
362 if !research.effect.top_quantiles.is_empty() {
364 writeln!(out, " Hotspots:").unwrap();
365 for tq in &research.effect.top_quantiles {
366 writeln!(
367 out,
368 " \u{2192} p{:.0} ({:.1}ns, {:.0}% confident)",
369 tq.quantile_p * 100.0,
370 tq.mean_ns,
371 tq.exceed_prob * 100.0
372 )
373 .unwrap();
374 }
375 }
376
377 if research.model_mismatch {
378 writeln!(out).unwrap();
379 writeln!(
380 out,
381 " {} Model mismatch detected \u{2013} interpret with caution",
382 yellow("\u{26A0}")
383 )
384 .unwrap();
385 }
386
387 format_diagnostics_section(out, &research.diagnostics);
389 format_reproduction_line(out, &research.diagnostics);
390 format_debug_environment(out, &research.diagnostics);
391}
392
393pub fn format_diagnostics_section(out: &mut String, diagnostics: &Diagnostics) {
399 writeln!(out).unwrap();
400 writeln!(out, "{}", SEPARATOR).unwrap();
401 writeln!(out).unwrap();
402 writeln!(out, " Measurement Diagnostics").unwrap();
403 writeln!(out).unwrap();
404
405 writeln!(
406 out,
407 " Dependence: block length {} (ESS: {} / {} raw)",
408 diagnostics.dependence_length,
409 diagnostics.effective_sample_size,
410 diagnostics.calibration_samples
411 )
412 .unwrap();
413
414 let stationarity_status = if diagnostics.stationarity_ok {
415 green("OK")
416 } else {
417 red("Suspect")
418 };
419 writeln!(
420 out,
421 " Stationarity: {:.2}x ({})",
422 diagnostics.stationarity_ratio, stationarity_status
423 )
424 .unwrap();
425
426 let mut outlier_line = format!(
427 " Outliers: baseline {:.2}%, sample {:.2}%",
428 diagnostics.outlier_rate_baseline * 100.0,
429 diagnostics.outlier_rate_sample * 100.0
430 );
431 if !diagnostics.outlier_asymmetry_ok {
432 outlier_line.push_str(&format!(" ({})", red("asymmetric")));
433 }
434 writeln!(out, "{}", outlier_line).unwrap();
435
436 writeln!(
437 out,
438 " Calibration: {} samples",
439 diagnostics.calibration_samples
440 )
441 .unwrap();
442
443 writeln!(out, " Runtime: {:.1}s", diagnostics.total_time_secs).unwrap();
444
445 if !diagnostics.warnings.is_empty() {
447 writeln!(out).unwrap();
448 writeln!(out, " {} Warnings", yellow("\u{26A0}")).unwrap();
449 for warning in &diagnostics.warnings {
450 writeln!(out, " \u{2022} {}", warning).unwrap();
451 }
452 }
453
454 if !diagnostics.quality_issues.is_empty() {
456 writeln!(out).unwrap();
457 writeln!(out, " {} Quality Issues", yellow("\u{26A0}")).unwrap();
458 for issue in &diagnostics.quality_issues {
459 let wrapped_msg = wrap_text(
460 &issue.message,
461 DEFAULT_WRAP_WIDTH,
462 20,
463 " ",
464 );
465 writeln!(
466 out,
467 " \u{2022} {}: {}",
468 bold(&format!("{:?}", issue.code)),
469 wrapped_msg
470 )
471 .unwrap();
472 let wrapped_guidance = wrap_text(&issue.guidance, DEFAULT_WRAP_WIDTH, 8, " ");
473 writeln!(out, " \u{2192} {}", dim(&wrapped_guidance)).unwrap();
474 }
475 }
476}
477
478pub fn format_reproduction_line(out: &mut String, diagnostics: &Diagnostics) {
480 let mut parts = Vec::new();
481
482 if let Some(ref model) = diagnostics.attacker_model {
483 parts.push(format!("model={}", model));
484 }
485 if diagnostics.threshold_ns > 0.0 {
486 parts.push(format!("\u{03B8}={:.0}ns", diagnostics.threshold_ns));
487 }
488 if !diagnostics.timer_name.is_empty() {
489 parts.push(format!("timer={}", diagnostics.timer_name));
490 }
491
492 if !parts.is_empty() {
493 writeln!(out).unwrap();
494 writeln!(out, " Reproduce: {}", parts.join(", ")).unwrap();
495 }
496}
497
498pub fn format_debug_environment(out: &mut String, diagnostics: &Diagnostics) {
500 writeln!(out).unwrap();
501 writeln!(out, "{}", SEPARATOR).unwrap();
502 writeln!(out).unwrap();
503 writeln!(out, " Debug Information").unwrap();
504 writeln!(out).unwrap();
505
506 writeln!(out, " Environment:").unwrap();
508 let platform = if diagnostics.platform.is_empty() {
509 #[cfg(feature = "std")]
510 {
511 format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
512 }
513 #[cfg(not(feature = "std"))]
514 {
515 "unknown".to_string()
516 }
517 } else {
518 diagnostics.platform.clone()
519 };
520 writeln!(out, " Platform: {}", platform).unwrap();
521 writeln!(out, " Rust version: 1.80").unwrap();
522 writeln!(out, " Package: tacet v0.1.5").unwrap();
523
524 writeln!(out).unwrap();
526 writeln!(out, " Configuration:").unwrap();
527 if let Some(ref model) = diagnostics.attacker_model {
528 writeln!(out, " Attacker model: {}", model).unwrap();
529 }
530 writeln!(
531 out,
532 " Threshold (\u{03B8}): {:.1} ns",
533 diagnostics.threshold_ns
534 )
535 .unwrap();
536 if !diagnostics.timer_name.is_empty() {
537 let timer_display = match &diagnostics.timer_fallback_reason {
539 Some(reason) => format!("{} (fallback: {})", diagnostics.timer_name, reason),
540 None => diagnostics.timer_name.clone(),
541 };
542 writeln!(out, " Timer: {}", timer_display).unwrap();
543 }
544 writeln!(
545 out,
546 " Resolution: {:.1} ns",
547 diagnostics.timer_resolution_ns
548 )
549 .unwrap();
550 if diagnostics.discrete_mode {
551 writeln!(out, " Discrete mode: enabled").unwrap();
552 }
553
554 writeln!(out).unwrap();
556 writeln!(out, " Statistical Summary:").unwrap();
557 writeln!(
558 out,
559 " Calibration: {} samples",
560 diagnostics.calibration_samples
561 )
562 .unwrap();
563 writeln!(
564 out,
565 " Block length: {}",
566 diagnostics.dependence_length
567 )
568 .unwrap();
569 writeln!(
570 out,
571 " ESS: {}",
572 diagnostics.effective_sample_size
573 )
574 .unwrap();
575 writeln!(
576 out,
577 " Stationarity: {:.2}x {}",
578 diagnostics.stationarity_ratio,
579 if diagnostics.stationarity_ok {
580 "OK"
581 } else {
582 "Suspect"
583 }
584 )
585 .unwrap();
586 writeln!(out).unwrap();
587 writeln!(
588 out,
589 " For bug reports, re-run with TIMING_ORACLE_DEBUG=1 and include both outputs"
590 )
591 .unwrap();
592}
593
594pub fn format_preflight_validation(out: &mut String, diagnostics: &Diagnostics) {
596 writeln!(out).unwrap();
597 writeln!(out, "{}", SEPARATOR).unwrap();
598 writeln!(out).unwrap();
599 writeln!(out, " Preflight Checks").unwrap();
600
601 let sanity: Vec<_> = diagnostics
603 .preflight_warnings
604 .iter()
605 .filter(|w| w.category == PreflightCategory::Sanity)
606 .collect();
607 let timer_sanity: Vec<_> = diagnostics
608 .preflight_warnings
609 .iter()
610 .filter(|w| w.category == PreflightCategory::TimerSanity)
611 .collect();
612 let autocorr: Vec<_> = diagnostics
613 .preflight_warnings
614 .iter()
615 .filter(|w| w.category == PreflightCategory::Autocorrelation)
616 .collect();
617 let system: Vec<_> = diagnostics
618 .preflight_warnings
619 .iter()
620 .filter(|w| w.category == PreflightCategory::System)
621 .collect();
622 let resolution: Vec<_> = diagnostics
623 .preflight_warnings
624 .iter()
625 .filter(|w| w.category == PreflightCategory::Resolution)
626 .collect();
627
628 writeln!(out).unwrap();
630 writeln!(out, " Result Integrity:").unwrap();
631
632 if sanity.is_empty() {
633 writeln!(out, " Sanity (F-vs-F): OK").unwrap();
634 } else {
635 for w in &sanity {
636 writeln!(
637 out,
638 " Sanity (F-vs-F): {}",
639 format_severity(w.severity)
640 )
641 .unwrap();
642 writeln!(out, " {}", w.message).unwrap();
643 }
644 }
645
646 if timer_sanity.is_empty() {
647 writeln!(out, " Timer monotonic: OK").unwrap();
648 } else {
649 for w in &timer_sanity {
650 writeln!(
651 out,
652 " Timer monotonic: {}",
653 format_severity(w.severity)
654 )
655 .unwrap();
656 writeln!(out, " {}", w.message).unwrap();
657 }
658 }
659
660 let stationarity_status = if diagnostics.stationarity_ok {
661 format!("OK {:.2}x", diagnostics.stationarity_ratio)
662 } else {
663 format!("Suspect {:.2}x", diagnostics.stationarity_ratio)
664 };
665 writeln!(out, " Stationarity: {}", stationarity_status).unwrap();
666
667 writeln!(out).unwrap();
669 writeln!(out, " Sampling Efficiency:").unwrap();
670
671 if autocorr.is_empty() {
672 writeln!(out, " Autocorrelation: OK").unwrap();
673 } else {
674 for w in &autocorr {
675 writeln!(
676 out,
677 " Autocorrelation: {}",
678 format_severity(w.severity)
679 )
680 .unwrap();
681 writeln!(out, " {}", w.message).unwrap();
682 }
683 }
684
685 let timer_name = if diagnostics.timer_name.is_empty() {
686 String::new()
687 } else {
688 format!(" ({})", diagnostics.timer_name)
689 };
690 if resolution.is_empty() {
691 writeln!(
692 out,
693 " Timer resolution: OK {:.1}ns{}",
694 diagnostics.timer_resolution_ns, timer_name
695 )
696 .unwrap();
697 } else {
698 for w in &resolution {
699 writeln!(
700 out,
701 " Timer resolution: {} {:.1}ns{}",
702 format_severity(w.severity),
703 diagnostics.timer_resolution_ns,
704 timer_name
705 )
706 .unwrap();
707 writeln!(out, " {}", w.message).unwrap();
708 }
709 }
710
711 writeln!(out).unwrap();
713 writeln!(out, " System:").unwrap();
714 if system.is_empty() {
715 writeln!(out, " Configuration: OK").unwrap();
716 } else {
717 for w in &system {
718 writeln!(out, " \u{26A0} {}", w.message).unwrap();
719 if let Some(guidance) = &w.guidance {
720 writeln!(out, " {}", guidance).unwrap();
721 }
722 }
723 }
724}
725
726fn format_preflight_notes(out: &mut String, diagnostics: &Diagnostics) {
728 if diagnostics.preflight_warnings.is_empty() {
729 return;
730 }
731
732 let has_critical = diagnostics
733 .preflight_warnings
734 .iter()
735 .any(|w| w.severity == PreflightSeverity::ResultUndermining);
736
737 writeln!(out).unwrap();
738 if has_critical {
739 writeln!(out, " \u{26A0} Measurement Notes:").unwrap();
740 } else {
741 writeln!(out, " Measurement Notes:").unwrap();
742 }
743
744 for warning in &diagnostics.preflight_warnings {
745 let bullet = match warning.severity {
746 PreflightSeverity::ResultUndermining => "\u{2022}",
747 PreflightSeverity::Informational => "\u{2022}",
748 };
749 writeln!(out, " {} {}", bullet, warning.message).unwrap();
750
751 let guidance = if warning.category == PreflightCategory::Resolution {
753 resolution_guidance_for_fallback(diagnostics.timer_fallback_reason.as_deref())
754 } else {
755 warning.guidance.clone()
756 };
757
758 if let Some(g) = guidance {
759 writeln!(out, " {}", g).unwrap();
760 }
761 }
762}
763
764fn resolution_guidance_for_fallback(fallback_reason: Option<&str>) -> Option<String> {
766 match fallback_reason {
767 Some("concurrent access") => Some(
768 "Cycle-accurate timing is locked by another process. \
769 If using cargo test, run with --test-threads=1."
770 .into(),
771 ),
772 Some("no sudo") => {
773 Some("Run with sudo to enable cycle-accurate timing (~0.3ns resolution).".into())
774 }
775 Some("unavailable") => Some(
776 "Cycle-accurate timing unavailable. Consider increasing max_samples \
777 or testing at a higher abstraction level."
778 .into(),
779 ),
780 Some("user requested") => Some(
781 "System timer was explicitly requested. For better resolution, \
782 use TimerSpec::Auto or TimerSpec::RequireCycleAccurate."
783 .into(),
784 ),
785 None => Some(
787 "Timer resolution may be limiting measurement quality. \
788 Consider increasing max_samples or time_budget."
789 .into(),
790 ),
791 Some(_) => Some("Timer resolution may be limiting measurement quality.".into()),
793 }
794}
795
796fn format_inconclusive_diagnostics(out: &mut String, diagnostics: &Diagnostics) {
798 let system_config: Vec<_> = diagnostics
799 .preflight_warnings
800 .iter()
801 .filter(|w| w.category == PreflightCategory::System)
802 .collect();
803 let resolution: Vec<_> = diagnostics
804 .preflight_warnings
805 .iter()
806 .filter(|w| w.category == PreflightCategory::Resolution)
807 .collect();
808
809 if system_config.is_empty() && resolution.is_empty() {
810 return;
811 }
812
813 writeln!(out).unwrap();
814 writeln!(out, " \u{2139} Why This May Have Happened:").unwrap();
815
816 if !system_config.is_empty() {
817 writeln!(out).unwrap();
818 writeln!(out, " System Configuration:").unwrap();
819 for warning in system_config {
820 writeln!(out, " \u{2022} {}", warning.message).unwrap();
821 }
822 }
823
824 if !resolution.is_empty() {
825 writeln!(out).unwrap();
826 writeln!(out, " Timer Resolution:").unwrap();
827 for warning in resolution {
828 writeln!(out, " \u{2022} {}", warning.message).unwrap();
829 if let Some(guidance) = &warning.guidance {
830 writeln!(out, " \u{2192} Tip: {}", guidance).unwrap();
831 }
832 }
833 }
834}
835
836fn format_debug_core_metrics(
841 out: &mut String,
842 leak_probability: f64,
843 effect: &EffectEstimate,
844 quality: MeasurementQuality,
845 samples_used: usize,
846 diagnostics: &Diagnostics,
847) {
848 writeln!(out, "\u{2502} P(leak) = {:.1}%", leak_probability * 100.0).unwrap();
849 writeln!(
850 out,
851 "\u{2502} Effect = {:.1}ns (CI: [{:.1}, {:.1}])",
852 effect.max_effect_ns, effect.credible_interval_ns.0, effect.credible_interval_ns.1
853 )
854 .unwrap();
855
856 let ess = diagnostics.effective_sample_size;
857 let efficiency = if samples_used > 0 {
858 libm::round(ess as f64 / samples_used as f64 * 100.0) as usize
859 } else {
860 0
861 };
862 writeln!(
863 out,
864 "\u{2502} Quality = {} (ESS: {} / {} raw, {}%)",
865 format_quality_colored(quality),
866 ess,
867 samples_used,
868 efficiency
869 )
870 .unwrap();
871}
872
873fn format_debug_warnings(out: &mut String, diagnostics: &Diagnostics) {
874 if diagnostics.warnings.is_empty() && diagnostics.quality_issues.is_empty() {
875 return;
876 }
877
878 writeln!(out, "\u{2502}").unwrap();
879 writeln!(out, "\u{2502} {} Warnings:", yellow("\u{26A0}")).unwrap();
880
881 for warning in &diagnostics.warnings {
882 writeln!(out, "\u{2502} \u{2022} {}", warning).unwrap();
883 }
884 for issue in &diagnostics.quality_issues {
885 writeln!(
886 out,
887 "\u{2502} \u{2022} {:?}: {}",
888 issue.code, issue.message
889 )
890 .unwrap();
891 }
892}
893
894fn format_debug_diagnostics(out: &mut String, diagnostics: &Diagnostics) {
895 writeln!(out, "\u{2502}").unwrap();
896 writeln!(out, "\u{2502} Diagnostics:").unwrap();
897
898 let timer_suffix = match &diagnostics.timer_fallback_reason {
900 Some(reason) => format!(" (fallback: {})", reason),
901 None => {
902 if diagnostics.discrete_mode {
903 " (discrete)".to_string()
904 } else {
905 String::new()
906 }
907 }
908 };
909 writeln!(
910 out,
911 "\u{2502} Timer: {}{}",
912 diagnostics.timer_name, timer_suffix
913 )
914 .unwrap();
915
916 let stationarity_status = if diagnostics.stationarity_ok {
917 format!("{:.1}x", diagnostics.stationarity_ratio)
918 } else {
919 format!("{:.1}x {}", diagnostics.stationarity_ratio, red("DRIFT"))
920 };
921 writeln!(out, "\u{2502} Stationarity: {}", stationarity_status).unwrap();
922
923 let outlier_note = if !diagnostics.outlier_asymmetry_ok {
924 format!(" ({})", red("asymmetric!"))
925 } else {
926 String::new()
927 };
928 writeln!(
929 out,
930 "\u{2502} Outliers: {:.1}% / {:.1}%{}",
931 diagnostics.outlier_rate_baseline * 100.0,
932 diagnostics.outlier_rate_sample * 100.0,
933 outlier_note
934 )
935 .unwrap();
936
937 writeln!(
938 out,
939 "\u{2502} Runtime: {:.1}s",
940 diagnostics.total_time_secs
941 )
942 .unwrap();
943}
944
945fn format_debug_research(out: &mut String, research: &ResearchOutcome) {
946 let status_str = match &research.status {
947 ResearchStatus::EffectDetected => green("Effect Detected"),
948 ResearchStatus::NoEffectDetected => green("No Effect Detected"),
949 ResearchStatus::ResolutionLimitReached => yellow("Resolution Limit"),
950 ResearchStatus::QualityIssue(_) => yellow("Quality Issue"),
951 ResearchStatus::BudgetExhausted => yellow("Budget Exhausted"),
952 };
953 writeln!(out, "\u{2502} Outcome = RESEARCH").unwrap();
954 writeln!(out, "\u{2502} Status = {}", status_str).unwrap();
955 writeln!(
956 out,
957 "\u{2502} Max Effect = {:.1}ns (CI: [{:.1}, {:.1}])",
958 research.max_effect_ns, research.max_effect_ci.0, research.max_effect_ci.1
959 )
960 .unwrap();
961 writeln!(
962 out,
963 "\u{2502} Floor = {:.1}ns, Detectable = {}",
964 research.theta_floor,
965 if research.detectable { "yes" } else { "no" }
966 )
967 .unwrap();
968 writeln!(
969 out,
970 "\u{2502} Effect = {:.1}ns (CI: [{:.1}, {:.1}])",
971 research.effect.max_effect_ns,
972 research.effect.credible_interval_ns.0,
973 research.effect.credible_interval_ns.1
974 )
975 .unwrap();
976
977 let ess = research.diagnostics.effective_sample_size;
978 let raw = research.samples_used;
979 let efficiency = if raw > 0 {
980 libm::round(ess as f64 / raw as f64 * 100.0) as usize
981 } else {
982 0
983 };
984 writeln!(
985 out,
986 "\u{2502} Quality = {} (ESS: {} / {} raw, {}%)",
987 format_quality_colored(research.quality),
988 ess,
989 raw,
990 efficiency
991 )
992 .unwrap();
993
994 if research.model_mismatch {
995 writeln!(out, "\u{2502}").unwrap();
996 writeln!(
997 out,
998 "\u{2502} {} Model mismatch detected",
999 yellow("\u{26A0}")
1000 )
1001 .unwrap();
1002 }
1003
1004 format_debug_warnings(out, &research.diagnostics);
1005
1006 writeln!(out, "\u{2502}").unwrap();
1007 writeln!(out, "\u{2502} Diagnostics:").unwrap();
1008 writeln!(
1009 out,
1010 "\u{2502} Timer: {:.1}ns resolution",
1011 research.diagnostics.timer_resolution_ns
1012 )
1013 .unwrap();
1014 writeln!(
1015 out,
1016 "\u{2502} Runtime: {:.1}s",
1017 research.diagnostics.total_time_secs
1018 )
1019 .unwrap();
1020}
1021
1022pub fn exploitability_info(exploit: Exploitability) -> (&'static str, &'static str) {
1028 match exploit {
1029 Exploitability::SharedHardwareOnly => {
1030 ("Shared hardware (SGX, containers)", "~1k on same core")
1031 }
1032 Exploitability::Http2Multiplexing => ("HTTP/2 multiplexing", "~100k concurrent"),
1033 Exploitability::StandardRemote => ("Standard remote timing", "~1k-10k"),
1034 Exploitability::ObviousLeak => ("Any (trivially observable)", "<100"),
1035 }
1036}
1037
1038fn exploitability_info_colored(exploit: Exploitability) -> (String, String) {
1040 match exploit {
1041 Exploitability::SharedHardwareOnly => (
1042 green("Shared hardware (SGX, containers)"),
1043 green("~1k on same core"),
1044 ),
1045 Exploitability::Http2Multiplexing => {
1046 (yellow("HTTP/2 multiplexing"), yellow("~100k concurrent"))
1047 }
1048 Exploitability::StandardRemote => (red("Standard remote timing"), red("~1k-10k")),
1049 Exploitability::ObviousLeak => (bold_red("Any (trivially observable)"), bold_red("<100")),
1050 }
1051}
1052
1053fn format_severity(severity: PreflightSeverity) -> &'static str {
1054 match severity {
1055 PreflightSeverity::ResultUndermining => "WARNING",
1056 PreflightSeverity::Informational => "INFO",
1057 }
1058}
1059
1060fn wrap_text(text: &str, width: usize, first_line_used: usize, cont_indent: &str) -> String {
1062 let first_available = width.saturating_sub(first_line_used);
1063 let cont_available = width.saturating_sub(cont_indent.len());
1064
1065 if first_available == 0 || cont_available == 0 {
1066 return text.to_string();
1067 }
1068
1069 let words: Vec<&str> = text.split_whitespace().collect();
1070 if words.is_empty() {
1071 return String::new();
1072 }
1073
1074 let mut lines = Vec::new();
1075 let mut current_line = String::new();
1076 let mut is_first_line = true;
1077
1078 for word in words {
1079 let available = if is_first_line {
1080 first_available
1081 } else {
1082 cont_available
1083 };
1084
1085 if current_line.is_empty() {
1086 current_line = word.to_string();
1087 } else if current_line.len() + 1 + word.len() <= available {
1088 current_line.push(' ');
1089 current_line.push_str(word);
1090 } else {
1091 lines.push((is_first_line, current_line));
1092 is_first_line = false;
1093 current_line = word.to_string();
1094 }
1095 }
1096
1097 if !current_line.is_empty() {
1098 lines.push((is_first_line, current_line));
1099 }
1100
1101 lines
1102 .into_iter()
1103 .map(|(is_first, line)| {
1104 if is_first {
1105 line
1106 } else {
1107 format!("{}{}", cont_indent, line)
1108 }
1109 })
1110 .collect::<Vec<_>>()
1111 .join("\n")
1112}