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 let magnitude = effect.total_effect_ns();
273 writeln!(out, " Effect: {:.1} ns ({})", magnitude, effect.pattern).unwrap();
274 writeln!(out, " Shift: {:.1} ns", effect.shift_ns).unwrap();
275 writeln!(out, " Tail: {:.1} ns", effect.tail_ns).unwrap();
276 writeln!(
277 out,
278 " 95% CI: {:.1}\u{2013}{:.1} ns",
279 effect.credible_interval_ns.0, effect.credible_interval_ns.1
280 )
281 .unwrap();
282
283 writeln!(out).unwrap();
284 writeln!(out, " Exploitability (heuristic):").unwrap();
285 let (vector, queries) = exploitability_info_colored(exploitability);
286 writeln!(out, " Attack vector: {}", vector).unwrap();
287 writeln!(out, " Queries needed: {}", queries).unwrap();
288}
289
290fn format_inconclusive_body(out: &mut String, leak_probability: f64, effect: &EffectEstimate) {
291 writeln!(
292 out,
293 " Current probability of leak: {:.1}%",
294 leak_probability * 100.0
295 )
296 .unwrap();
297
298 let magnitude = effect.total_effect_ns();
299 writeln!(
300 out,
301 " Effect estimate: {:.1} ns ({})",
302 magnitude, effect.pattern
303 )
304 .unwrap();
305}
306
307fn format_research_outcome(out: &mut String, research: &ResearchOutcome) {
308 writeln!(out, " Samples: {} per class", research.samples_used).unwrap();
309 writeln!(
310 out,
311 " Quality: {}",
312 format_quality_colored(research.quality)
313 )
314 .unwrap();
315 writeln!(out).unwrap();
316
317 let status_line = match &research.status {
318 ResearchStatus::EffectDetected => bold_green("\u{1F50D} Effect Detected"),
319 ResearchStatus::NoEffectDetected => bold_cyan("\u{2713} No Effect Detected"),
320 ResearchStatus::ResolutionLimitReached => bold_yellow("\u{26A0} Resolution Limit Reached"),
321 ResearchStatus::QualityIssue(_) => bold_yellow("? Quality Issue"),
322 ResearchStatus::BudgetExhausted => bold_cyan("? Budget Exhausted"),
323 };
324 writeln!(out, " {}", status_line).unwrap();
325 writeln!(out).unwrap();
326
327 writeln!(out, " Max effect: {:.1} ns", research.max_effect_ns).unwrap();
328 writeln!(
329 out,
330 " 95% CI: [{:.1}, {:.1}] ns",
331 research.max_effect_ci.0, research.max_effect_ci.1
332 )
333 .unwrap();
334 writeln!(out, " Measurement floor: {:.1} ns", research.theta_floor).unwrap();
335 writeln!(
336 out,
337 " Detectable: {}",
338 if research.detectable { "yes" } else { "no" }
339 )
340 .unwrap();
341
342 writeln!(out).unwrap();
343 writeln!(
344 out,
345 " Effect: {:.1} ns ({})",
346 research.effect.total_effect_ns(),
347 research.effect.pattern
348 )
349 .unwrap();
350 writeln!(out, " Shift: {:.1} ns", research.effect.shift_ns).unwrap();
351 writeln!(out, " Tail: {:.1} ns", research.effect.tail_ns).unwrap();
352
353 if research.model_mismatch {
354 writeln!(out).unwrap();
355 writeln!(
356 out,
357 " {} Model mismatch detected \u{2013} interpret with caution",
358 yellow("\u{26A0}")
359 )
360 .unwrap();
361 if let Some(ref caveat) = research.effect.interpretation_caveat {
362 writeln!(out, " {}", caveat).unwrap();
363 }
364 }
365
366 format_diagnostics_section(out, &research.diagnostics);
368 format_reproduction_line(out, &research.diagnostics);
369 format_debug_environment(out, &research.diagnostics);
370}
371
372pub fn format_diagnostics_section(out: &mut String, diagnostics: &Diagnostics) {
378 writeln!(out).unwrap();
379 writeln!(out, "{}", SEPARATOR).unwrap();
380 writeln!(out).unwrap();
381 writeln!(out, " Measurement Diagnostics").unwrap();
382 writeln!(out).unwrap();
383
384 writeln!(
385 out,
386 " Dependence: block length {} (ESS: {} / {} raw)",
387 diagnostics.dependence_length,
388 diagnostics.effective_sample_size,
389 diagnostics.calibration_samples
390 )
391 .unwrap();
392
393 let projection_status = if diagnostics.projection_mismatch_ok {
394 green("OK")
395 } else {
396 red("Mismatch")
397 };
398 writeln!(
399 out,
400 " Projection: Q = {:.1} ({})",
401 diagnostics.projection_mismatch_q, projection_status
402 )
403 .unwrap();
404
405 let mut outlier_line = format!(
406 " Outliers: baseline {:.2}%, sample {:.2}%",
407 diagnostics.outlier_rate_baseline * 100.0,
408 diagnostics.outlier_rate_sample * 100.0
409 );
410 if !diagnostics.outlier_asymmetry_ok {
411 outlier_line.push_str(&format!(" ({})", red("asymmetric")));
412 }
413 writeln!(out, "{}", outlier_line).unwrap();
414
415 writeln!(
416 out,
417 " Calibration: {} samples",
418 diagnostics.calibration_samples
419 )
420 .unwrap();
421
422 writeln!(out, " Runtime: {:.1}s", diagnostics.total_time_secs).unwrap();
423
424 if !diagnostics.warnings.is_empty() {
426 writeln!(out).unwrap();
427 writeln!(out, " {} Warnings", yellow("\u{26A0}")).unwrap();
428 for warning in &diagnostics.warnings {
429 writeln!(out, " \u{2022} {}", warning).unwrap();
430 }
431 }
432
433 if !diagnostics.quality_issues.is_empty() {
435 writeln!(out).unwrap();
436 writeln!(out, " {} Quality Issues", yellow("\u{26A0}")).unwrap();
437 for issue in &diagnostics.quality_issues {
438 let wrapped_msg = wrap_text(
439 &issue.message,
440 DEFAULT_WRAP_WIDTH,
441 20,
442 " ",
443 );
444 writeln!(
445 out,
446 " \u{2022} {}: {}",
447 bold(&format!("{:?}", issue.code)),
448 wrapped_msg
449 )
450 .unwrap();
451 let wrapped_guidance = wrap_text(&issue.guidance, DEFAULT_WRAP_WIDTH, 8, " ");
452 writeln!(out, " \u{2192} {}", dim(&wrapped_guidance)).unwrap();
453 }
454 }
455}
456
457pub fn format_reproduction_line(out: &mut String, diagnostics: &Diagnostics) {
459 let mut parts = Vec::new();
460
461 if let Some(ref model) = diagnostics.attacker_model {
462 parts.push(format!("model={}", model));
463 }
464 if diagnostics.threshold_ns > 0.0 {
465 parts.push(format!("\u{03B8}={:.0}ns", diagnostics.threshold_ns));
466 }
467 if !diagnostics.timer_name.is_empty() {
468 parts.push(format!("timer={}", diagnostics.timer_name));
469 }
470
471 if !parts.is_empty() {
472 writeln!(out).unwrap();
473 writeln!(out, " Reproduce: {}", parts.join(", ")).unwrap();
474 }
475}
476
477pub fn format_debug_environment(out: &mut String, diagnostics: &Diagnostics) {
479 writeln!(out).unwrap();
480 writeln!(out, "{}", SEPARATOR).unwrap();
481 writeln!(out).unwrap();
482 writeln!(out, " Debug Information").unwrap();
483 writeln!(out).unwrap();
484
485 writeln!(out, " Environment:").unwrap();
487 let platform = if diagnostics.platform.is_empty() {
488 #[cfg(feature = "std")]
489 {
490 format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
491 }
492 #[cfg(not(feature = "std"))]
493 {
494 "unknown".to_string()
495 }
496 } else {
497 diagnostics.platform.clone()
498 };
499 writeln!(out, " Platform: {}", platform).unwrap();
500 writeln!(out, " Rust version: 1.80").unwrap();
501 writeln!(out, " Package: tacet v0.1.5").unwrap();
502
503 writeln!(out).unwrap();
505 writeln!(out, " Configuration:").unwrap();
506 if let Some(ref model) = diagnostics.attacker_model {
507 writeln!(out, " Attacker model: {}", model).unwrap();
508 }
509 writeln!(
510 out,
511 " Threshold (\u{03B8}): {:.1} ns",
512 diagnostics.threshold_ns
513 )
514 .unwrap();
515 if !diagnostics.timer_name.is_empty() {
516 let timer_display = match &diagnostics.timer_fallback_reason {
518 Some(reason) => format!("{} (fallback: {})", diagnostics.timer_name, reason),
519 None => diagnostics.timer_name.clone(),
520 };
521 writeln!(out, " Timer: {}", timer_display).unwrap();
522 }
523 writeln!(
524 out,
525 " Resolution: {:.1} ns",
526 diagnostics.timer_resolution_ns
527 )
528 .unwrap();
529 if diagnostics.discrete_mode {
530 writeln!(out, " Discrete mode: enabled").unwrap();
531 }
532
533 writeln!(out).unwrap();
535 writeln!(out, " Statistical Summary:").unwrap();
536 writeln!(
537 out,
538 " Calibration: {} samples",
539 diagnostics.calibration_samples
540 )
541 .unwrap();
542 writeln!(
543 out,
544 " Block length: {}",
545 diagnostics.dependence_length
546 )
547 .unwrap();
548 writeln!(
549 out,
550 " ESS: {}",
551 diagnostics.effective_sample_size
552 )
553 .unwrap();
554 writeln!(
555 out,
556 " Stationarity: {:.2}x {}",
557 diagnostics.stationarity_ratio,
558 if diagnostics.stationarity_ok {
559 "OK"
560 } else {
561 "Suspect"
562 }
563 )
564 .unwrap();
565 writeln!(
566 out,
567 " Projection: Q={:.1} {}",
568 diagnostics.projection_mismatch_q,
569 if diagnostics.projection_mismatch_ok {
570 "OK"
571 } else {
572 "Mismatch"
573 }
574 )
575 .unwrap();
576
577 writeln!(out).unwrap();
578 writeln!(
579 out,
580 " For bug reports, re-run with TIMING_ORACLE_DEBUG=1 and include both outputs"
581 )
582 .unwrap();
583}
584
585pub fn format_preflight_validation(out: &mut String, diagnostics: &Diagnostics) {
587 writeln!(out).unwrap();
588 writeln!(out, "{}", SEPARATOR).unwrap();
589 writeln!(out).unwrap();
590 writeln!(out, " Preflight Checks").unwrap();
591
592 let sanity: Vec<_> = diagnostics
594 .preflight_warnings
595 .iter()
596 .filter(|w| w.category == PreflightCategory::Sanity)
597 .collect();
598 let timer_sanity: Vec<_> = diagnostics
599 .preflight_warnings
600 .iter()
601 .filter(|w| w.category == PreflightCategory::TimerSanity)
602 .collect();
603 let autocorr: Vec<_> = diagnostics
604 .preflight_warnings
605 .iter()
606 .filter(|w| w.category == PreflightCategory::Autocorrelation)
607 .collect();
608 let system: Vec<_> = diagnostics
609 .preflight_warnings
610 .iter()
611 .filter(|w| w.category == PreflightCategory::System)
612 .collect();
613 let resolution: Vec<_> = diagnostics
614 .preflight_warnings
615 .iter()
616 .filter(|w| w.category == PreflightCategory::Resolution)
617 .collect();
618
619 writeln!(out).unwrap();
621 writeln!(out, " Result Integrity:").unwrap();
622
623 if sanity.is_empty() {
624 writeln!(out, " Sanity (F-vs-F): OK").unwrap();
625 } else {
626 for w in &sanity {
627 writeln!(
628 out,
629 " Sanity (F-vs-F): {}",
630 format_severity(w.severity)
631 )
632 .unwrap();
633 writeln!(out, " {}", w.message).unwrap();
634 }
635 }
636
637 if timer_sanity.is_empty() {
638 writeln!(out, " Timer monotonic: OK").unwrap();
639 } else {
640 for w in &timer_sanity {
641 writeln!(
642 out,
643 " Timer monotonic: {}",
644 format_severity(w.severity)
645 )
646 .unwrap();
647 writeln!(out, " {}", w.message).unwrap();
648 }
649 }
650
651 let stationarity_status = if diagnostics.stationarity_ok {
652 format!("OK {:.2}x", diagnostics.stationarity_ratio)
653 } else {
654 format!("Suspect {:.2}x", diagnostics.stationarity_ratio)
655 };
656 writeln!(out, " Stationarity: {}", stationarity_status).unwrap();
657
658 writeln!(out).unwrap();
660 writeln!(out, " Sampling Efficiency:").unwrap();
661
662 if autocorr.is_empty() {
663 writeln!(out, " Autocorrelation: OK").unwrap();
664 } else {
665 for w in &autocorr {
666 writeln!(
667 out,
668 " Autocorrelation: {}",
669 format_severity(w.severity)
670 )
671 .unwrap();
672 writeln!(out, " {}", w.message).unwrap();
673 }
674 }
675
676 let timer_name = if diagnostics.timer_name.is_empty() {
677 String::new()
678 } else {
679 format!(" ({})", diagnostics.timer_name)
680 };
681 if resolution.is_empty() {
682 writeln!(
683 out,
684 " Timer resolution: OK {:.1}ns{}",
685 diagnostics.timer_resolution_ns, timer_name
686 )
687 .unwrap();
688 } else {
689 for w in &resolution {
690 writeln!(
691 out,
692 " Timer resolution: {} {:.1}ns{}",
693 format_severity(w.severity),
694 diagnostics.timer_resolution_ns,
695 timer_name
696 )
697 .unwrap();
698 writeln!(out, " {}", w.message).unwrap();
699 }
700 }
701
702 writeln!(out).unwrap();
704 writeln!(out, " System:").unwrap();
705 if system.is_empty() {
706 writeln!(out, " Configuration: OK").unwrap();
707 } else {
708 for w in &system {
709 writeln!(out, " \u{26A0} {}", w.message).unwrap();
710 if let Some(guidance) = &w.guidance {
711 writeln!(out, " {}", guidance).unwrap();
712 }
713 }
714 }
715}
716
717fn format_preflight_notes(out: &mut String, diagnostics: &Diagnostics) {
719 if diagnostics.preflight_warnings.is_empty() {
720 return;
721 }
722
723 let has_critical = diagnostics
724 .preflight_warnings
725 .iter()
726 .any(|w| w.severity == PreflightSeverity::ResultUndermining);
727
728 writeln!(out).unwrap();
729 if has_critical {
730 writeln!(out, " \u{26A0} Measurement Notes:").unwrap();
731 } else {
732 writeln!(out, " Measurement Notes:").unwrap();
733 }
734
735 for warning in &diagnostics.preflight_warnings {
736 let bullet = match warning.severity {
737 PreflightSeverity::ResultUndermining => "\u{2022}",
738 PreflightSeverity::Informational => "\u{2022}",
739 };
740 writeln!(out, " {} {}", bullet, warning.message).unwrap();
741
742 let guidance = if warning.category == PreflightCategory::Resolution {
744 resolution_guidance_for_fallback(diagnostics.timer_fallback_reason.as_deref())
745 } else {
746 warning.guidance.clone()
747 };
748
749 if let Some(g) = guidance {
750 writeln!(out, " {}", g).unwrap();
751 }
752 }
753}
754
755fn resolution_guidance_for_fallback(fallback_reason: Option<&str>) -> Option<String> {
757 match fallback_reason {
758 Some("concurrent access") => Some(
759 "Cycle-accurate timing is locked by another process. \
760 If using cargo test, run with --test-threads=1."
761 .into(),
762 ),
763 Some("no sudo") => {
764 Some("Run with sudo to enable cycle-accurate timing (~0.3ns resolution).".into())
765 }
766 Some("unavailable") => Some(
767 "Cycle-accurate timing unavailable. Consider increasing max_samples \
768 or testing at a higher abstraction level."
769 .into(),
770 ),
771 Some("user requested") => Some(
772 "System timer was explicitly requested. For better resolution, \
773 use TimerSpec::Auto or TimerSpec::RequireCycleAccurate."
774 .into(),
775 ),
776 None => Some(
778 "Timer resolution may be limiting measurement quality. \
779 Consider increasing max_samples or time_budget."
780 .into(),
781 ),
782 Some(_) => Some("Timer resolution may be limiting measurement quality.".into()),
784 }
785}
786
787fn format_inconclusive_diagnostics(out: &mut String, diagnostics: &Diagnostics) {
789 let system_config: Vec<_> = diagnostics
790 .preflight_warnings
791 .iter()
792 .filter(|w| w.category == PreflightCategory::System)
793 .collect();
794 let resolution: Vec<_> = diagnostics
795 .preflight_warnings
796 .iter()
797 .filter(|w| w.category == PreflightCategory::Resolution)
798 .collect();
799
800 if system_config.is_empty() && resolution.is_empty() {
801 return;
802 }
803
804 writeln!(out).unwrap();
805 writeln!(out, " \u{2139} Why This May Have Happened:").unwrap();
806
807 if !system_config.is_empty() {
808 writeln!(out).unwrap();
809 writeln!(out, " System Configuration:").unwrap();
810 for warning in system_config {
811 writeln!(out, " \u{2022} {}", warning.message).unwrap();
812 }
813 }
814
815 if !resolution.is_empty() {
816 writeln!(out).unwrap();
817 writeln!(out, " Timer Resolution:").unwrap();
818 for warning in resolution {
819 writeln!(out, " \u{2022} {}", warning.message).unwrap();
820 if let Some(guidance) = &warning.guidance {
821 writeln!(out, " \u{2192} Tip: {}", guidance).unwrap();
822 }
823 }
824 }
825}
826
827fn format_debug_core_metrics(
832 out: &mut String,
833 leak_probability: f64,
834 effect: &EffectEstimate,
835 quality: MeasurementQuality,
836 samples_used: usize,
837 diagnostics: &Diagnostics,
838) {
839 writeln!(out, "\u{2502} P(leak) = {:.1}%", leak_probability * 100.0).unwrap();
840 writeln!(
841 out,
842 "\u{2502} Effect = {:.1}ns shift + {:.1}ns tail ({})",
843 effect.shift_ns, effect.tail_ns, effect.pattern
844 )
845 .unwrap();
846
847 let ess = diagnostics.effective_sample_size;
848 let efficiency = if samples_used > 0 {
849 libm::round(ess as f64 / samples_used as f64 * 100.0) as usize
850 } else {
851 0
852 };
853 writeln!(
854 out,
855 "\u{2502} Quality = {} (ESS: {} / {} raw, {}%)",
856 format_quality_colored(quality),
857 ess,
858 samples_used,
859 efficiency
860 )
861 .unwrap();
862}
863
864fn format_debug_warnings(out: &mut String, diagnostics: &Diagnostics) {
865 if diagnostics.warnings.is_empty() && diagnostics.quality_issues.is_empty() {
866 return;
867 }
868
869 writeln!(out, "\u{2502}").unwrap();
870 writeln!(out, "\u{2502} {} Warnings:", yellow("\u{26A0}")).unwrap();
871
872 for warning in &diagnostics.warnings {
873 writeln!(out, "\u{2502} \u{2022} {}", warning).unwrap();
874 }
875 for issue in &diagnostics.quality_issues {
876 writeln!(
877 out,
878 "\u{2502} \u{2022} {:?}: {}",
879 issue.code, issue.message
880 )
881 .unwrap();
882 }
883}
884
885fn format_debug_diagnostics(out: &mut String, diagnostics: &Diagnostics) {
886 writeln!(out, "\u{2502}").unwrap();
887 writeln!(out, "\u{2502} Diagnostics:").unwrap();
888
889 let timer_suffix = match &diagnostics.timer_fallback_reason {
891 Some(reason) => format!(" (fallback: {})", reason),
892 None => {
893 if diagnostics.discrete_mode {
894 " (discrete)".to_string()
895 } else {
896 String::new()
897 }
898 }
899 };
900 writeln!(
901 out,
902 "\u{2502} Timer: {}{}",
903 diagnostics.timer_name, timer_suffix
904 )
905 .unwrap();
906
907 let projection_status = if diagnostics.projection_mismatch_ok {
908 green("OK")
909 } else {
910 red("MISMATCH")
911 };
912 writeln!(
913 out,
914 "\u{2502} Projection: Q = {:.1} ({})",
915 diagnostics.projection_mismatch_q, projection_status
916 )
917 .unwrap();
918
919 let stationarity_status = if diagnostics.stationarity_ok {
920 format!("{:.1}x", diagnostics.stationarity_ratio)
921 } else {
922 format!("{:.1}x {}", diagnostics.stationarity_ratio, red("DRIFT"))
923 };
924 writeln!(out, "\u{2502} Stationarity: {}", stationarity_status).unwrap();
925
926 let outlier_note = if !diagnostics.outlier_asymmetry_ok {
927 format!(" ({})", red("asymmetric!"))
928 } else {
929 String::new()
930 };
931 writeln!(
932 out,
933 "\u{2502} Outliers: {:.1}% / {:.1}%{}",
934 diagnostics.outlier_rate_baseline * 100.0,
935 diagnostics.outlier_rate_sample * 100.0,
936 outlier_note
937 )
938 .unwrap();
939
940 writeln!(
941 out,
942 "\u{2502} Runtime: {:.1}s",
943 diagnostics.total_time_secs
944 )
945 .unwrap();
946}
947
948fn format_debug_research(out: &mut String, research: &ResearchOutcome) {
949 let status_str = match &research.status {
950 ResearchStatus::EffectDetected => green("Effect Detected"),
951 ResearchStatus::NoEffectDetected => green("No Effect Detected"),
952 ResearchStatus::ResolutionLimitReached => yellow("Resolution Limit"),
953 ResearchStatus::QualityIssue(_) => yellow("Quality Issue"),
954 ResearchStatus::BudgetExhausted => yellow("Budget Exhausted"),
955 };
956 writeln!(out, "\u{2502} Outcome = RESEARCH").unwrap();
957 writeln!(out, "\u{2502} Status = {}", status_str).unwrap();
958 writeln!(
959 out,
960 "\u{2502} Max Effect = {:.1}ns (CI: [{:.1}, {:.1}])",
961 research.max_effect_ns, research.max_effect_ci.0, research.max_effect_ci.1
962 )
963 .unwrap();
964 writeln!(
965 out,
966 "\u{2502} Floor = {:.1}ns, Detectable = {}",
967 research.theta_floor,
968 if research.detectable { "yes" } else { "no" }
969 )
970 .unwrap();
971 writeln!(
972 out,
973 "\u{2502} Effect = {:.1}ns shift + {:.1}ns tail ({})",
974 research.effect.shift_ns, research.effect.tail_ns, research.effect.pattern
975 )
976 .unwrap();
977
978 let ess = research.diagnostics.effective_sample_size;
979 let raw = research.samples_used;
980 let efficiency = if raw > 0 {
981 libm::round(ess as f64 / raw as f64 * 100.0) as usize
982 } else {
983 0
984 };
985 writeln!(
986 out,
987 "\u{2502} Quality = {} (ESS: {} / {} raw, {}%)",
988 format_quality_colored(research.quality),
989 ess,
990 raw,
991 efficiency
992 )
993 .unwrap();
994
995 if research.model_mismatch {
996 writeln!(out, "\u{2502}").unwrap();
997 writeln!(
998 out,
999 "\u{2502} {} Model mismatch detected",
1000 yellow("\u{26A0}")
1001 )
1002 .unwrap();
1003 }
1004
1005 format_debug_warnings(out, &research.diagnostics);
1006
1007 writeln!(out, "\u{2502}").unwrap();
1008 writeln!(out, "\u{2502} Diagnostics:").unwrap();
1009 writeln!(
1010 out,
1011 "\u{2502} Timer: {:.1}ns resolution",
1012 research.diagnostics.timer_resolution_ns
1013 )
1014 .unwrap();
1015 writeln!(
1016 out,
1017 "\u{2502} Runtime: {:.1}s",
1018 research.diagnostics.total_time_secs
1019 )
1020 .unwrap();
1021}
1022
1023pub fn exploitability_info(exploit: Exploitability) -> (&'static str, &'static str) {
1029 match exploit {
1030 Exploitability::SharedHardwareOnly => {
1031 ("Shared hardware (SGX, containers)", "~1k on same core")
1032 }
1033 Exploitability::Http2Multiplexing => ("HTTP/2 multiplexing", "~100k concurrent"),
1034 Exploitability::StandardRemote => ("Standard remote timing", "~1k-10k"),
1035 Exploitability::ObviousLeak => ("Any (trivially observable)", "<100"),
1036 }
1037}
1038
1039fn exploitability_info_colored(exploit: Exploitability) -> (String, String) {
1041 match exploit {
1042 Exploitability::SharedHardwareOnly => (
1043 green("Shared hardware (SGX, containers)"),
1044 green("~1k on same core"),
1045 ),
1046 Exploitability::Http2Multiplexing => {
1047 (yellow("HTTP/2 multiplexing"), yellow("~100k concurrent"))
1048 }
1049 Exploitability::StandardRemote => (red("Standard remote timing"), red("~1k-10k")),
1050 Exploitability::ObviousLeak => (bold_red("Any (trivially observable)"), bold_red("<100")),
1051 }
1052}
1053
1054fn format_severity(severity: PreflightSeverity) -> &'static str {
1055 match severity {
1056 PreflightSeverity::ResultUndermining => "WARNING",
1057 PreflightSeverity::Informational => "INFO",
1058 }
1059}
1060
1061fn wrap_text(text: &str, width: usize, first_line_used: usize, cont_indent: &str) -> String {
1063 let first_available = width.saturating_sub(first_line_used);
1064 let cont_available = width.saturating_sub(cont_indent.len());
1065
1066 if first_available == 0 || cont_available == 0 {
1067 return text.to_string();
1068 }
1069
1070 let words: Vec<&str> = text.split_whitespace().collect();
1071 if words.is_empty() {
1072 return String::new();
1073 }
1074
1075 let mut lines = Vec::new();
1076 let mut current_line = String::new();
1077 let mut is_first_line = true;
1078
1079 for word in words {
1080 let available = if is_first_line {
1081 first_available
1082 } else {
1083 cont_available
1084 };
1085
1086 if current_line.is_empty() {
1087 current_line = word.to_string();
1088 } else if current_line.len() + 1 + word.len() <= available {
1089 current_line.push(' ');
1090 current_line.push_str(word);
1091 } else {
1092 lines.push((is_first_line, current_line));
1093 is_first_line = false;
1094 current_line = word.to_string();
1095 }
1096 }
1097
1098 if !current_line.is_empty() {
1099 lines.push((is_first_line, current_line));
1100 }
1101
1102 lines
1103 .into_iter()
1104 .map(|(is_first, line)| {
1105 if is_first {
1106 line
1107 } else {
1108 format!("{}{}", cont_indent, line)
1109 }
1110 })
1111 .collect::<Vec<_>>()
1112 .join("\n")
1113}