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