facett-binview/src/lib.rs
//! # facett-binview — the shared **binary-analysis** pane
//!
//! The ONE robot-testable egui [`Facet`](facett_core::Facet) that renders a built
//! artifact's **size + hardening** story, so every suite surface that wants to show
//! "what's in this binary and how did it grow" (dwarves' build view · nornir's
//! warehouse deck) is **one pane, not a per-app twin** (LAW #5 — DRY). It renders
//! four layers the nordisk binary-analysis subsystem produces:
//!
//! 1. **Bloat treemap** — nested rects sized by per-crate/-symbol bytes, coloured by
//! crate (a squarified treemap; the biggest crate gets the biggest rect).
//! 2. **Checksec badges** — a green/red chip per hardening protection
//! (RELRO / PIE / NX / stack-canary / stripped / FORTIFY).
//! 3. **Diff waterfall** — before→after size, each section's delta a floating bar
//! (growth vs shrink coloured), anchored by the before/after totals.
//! 4. **Size sparkline** — the artifact's byte size across recent builds.
//!
//! ## Layering — the pane is WAREHOUSE-AGNOSTIC (important)
//! facett carries **no** nornir / warehouse / dwarves dependency. The pane is
//! **presentational + host-driven**: the HOST implements the [`BinaryDataSeam`]
//! trait (feeding rows from the warehouse's `binary_facts` / `section_sizes` /
//! `symbol_sizes` / `artifact_diff`), and [`BinaryPane`] only ever **renders the
//! rows the seam surfaces**. This mirrors how the Release tab takes a
//! `ReleaseConvergence` in and the auth pane takes a `LoginState` in — the pane
//! never reaches for a backend.
//!
//! ## What it renders (per seam call)
//! * [`BinaryDataSeam::checksec`] → the hardening badges.
//! * [`BinaryDataSeam::crate_sizes`] (+ [`symbol_sizes`](BinaryDataSeam::symbol_sizes)
//! as leaves) → the bloat treemap.
//! * [`BinaryDataSeam::diff`] → the before→after waterfall (skipped when `None`).
//! * [`BinaryDataSeam::size_history`] → the sparkline.
//!
//! A network-free [`StubBinaryData`] drives the shipped demo + the robot test.
use egui::{Color32, FontId, Pos2, Rect, Sense, Stroke, Vec2};
use facett_core::look::oklch::Oklch;
use serde::{Deserialize, Serialize};
pub use facett_core::Facet;
// ─────────────────────────────────────────────────────────────────────────────
// Data rows — the neutral shapes the HOST feeds over the seam. These mirror the
// binary-analysis subsystem's warehouse tables (section_sizes / symbol_sizes /
// binary_facts / artifact_diff) but carry NO warehouse type dependency.
// ─────────────────────────────────────────────────────────────────────────────
/// One ELF/Mach-O section's on-disk size (`.text`, `.rodata`, `.data`, …).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SectionSize {
/// Section name (e.g. `".text"`).
pub name: String,
/// Byte size of the section.
pub bytes: u64,
}
impl SectionSize {
/// A `name`/`bytes` section row.
pub fn new(name: impl Into<String>, bytes: u64) -> Self {
Self { name: name.into(), bytes }
}
}
/// A per-crate bloat aggregate (the sum of the crate's symbols) — the treemap's
/// top-level tiles.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrateSize {
/// Crate name (e.g. `"nornir"`, `"std"`, `"[Unknown]"`).
pub crate_name: String,
/// Total bytes attributed to this crate.
pub bytes: u64,
}
impl CrateSize {
/// A `crate_name`/`bytes` aggregate row.
pub fn new(crate_name: impl Into<String>, bytes: u64) -> Self {
Self { crate_name: crate_name.into(), bytes }
}
}
/// A single symbol's size, attributed to its crate — the treemap's leaves and the
/// per-symbol bloat detail.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymbolSize {
/// The owning crate (may be `"[Unknown]"`).
pub crate_name: String,
/// The (possibly demangled) symbol name.
pub symbol: String,
/// Byte size of the symbol.
pub bytes: u64,
}
impl SymbolSize {
/// A `crate`/`symbol`/`bytes` leaf row.
pub fn new(crate_name: impl Into<String>, symbol: impl Into<String>, bytes: u64) -> Self {
Self { crate_name: crate_name.into(), symbol: symbol.into(), bytes }
}
}
/// RELRO hardening level (checksec's tri-state).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Relro {
/// Full RELRO — the GOT is read-only after startup (best).
Full,
/// Partial RELRO — some relocation sections read-only.
Partial,
/// No RELRO.
None,
}
impl Relro {
/// A short label for the badge / JSON.
pub fn label(self) -> &'static str {
match self {
Relro::Full => "full",
Relro::Partial => "partial",
Relro::None => "none",
}
}
/// Only **Full** counts as green; partial/none read as not-ok.
pub fn is_ok(self) -> bool {
matches!(self, Relro::Full)
}
}
/// The `checksec`-style hardening verdict from `binary_facts`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Checksec {
/// RELRO level.
pub relro: Relro,
/// Position-independent executable.
pub pie: bool,
/// Non-executable stack (NX / DEP).
pub nx: bool,
/// Stack canary present.
pub stack_canary: bool,
/// Symbol table stripped.
pub stripped: bool,
/// FORTIFY_SOURCE present.
pub fortify: bool,
}
impl Default for Checksec {
fn default() -> Self {
Self {
relro: Relro::None,
pie: false,
nx: false,
stack_canary: false,
stripped: false,
fortify: false,
}
}
}
/// One rendered hardening badge — a label + a green(ok)/red(not-ok) verdict + a
/// short detail. The pane paints these as coloured chips; the JSON carries them
/// verbatim (the RED-when-broken hook for the robot test).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckBadge {
/// The protection's short name (e.g. `"PIE"`).
pub label: &'static str,
/// `true` = protection present / hardened (green), `false` = absent (red).
pub ok: bool,
/// A short human detail (e.g. `"enabled"`, `"none"`).
pub detail: String,
}
impl Checksec {
/// The badges in a stable order — the exact chips the pane paints.
pub fn badges(&self) -> Vec<CheckBadge> {
let flag = |label, ok: bool| CheckBadge {
label,
ok,
detail: if ok { "enabled" } else { "disabled" }.to_string(),
};
vec![
CheckBadge { label: "RELRO", ok: self.relro.is_ok(), detail: self.relro.label().to_string() },
flag("PIE", self.pie),
flag("NX", self.nx),
flag("CANARY", self.stack_canary),
// Stripped is a size/hygiene win here (symbols removed) → green when true.
CheckBadge {
label: "STRIPPED",
ok: self.stripped,
detail: if self.stripped { "stripped" } else { "symbols" }.to_string(),
},
flag("FORTIFY", self.fortify),
]
}
}
/// One section's before→after size change — a step in the diff waterfall.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SectionDelta {
/// Section name.
pub name: String,
/// Bytes before.
pub before: u64,
/// Bytes after.
pub after: u64,
}
impl SectionDelta {
/// A `name`/`before`/`after` step.
pub fn new(name: impl Into<String>, before: u64, after: u64) -> Self {
Self { name: name.into(), before, after }
}
/// Signed byte delta (`after - before`).
pub fn delta(&self) -> i64 {
self.after as i64 - self.before as i64
}
}
/// One symbol's before→after change (grown or shrunk).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymbolDelta {
/// The symbol name.
pub symbol: String,
/// Bytes before.
pub before: u64,
/// Bytes after.
pub after: u64,
}
impl SymbolDelta {
/// A `symbol`/`before`/`after` change.
pub fn new(symbol: impl Into<String>, before: u64, after: u64) -> Self {
Self { symbol: symbol.into(), before, after }
}
/// Signed byte delta (`after - before`).
pub fn delta(&self) -> i64 {
self.after as i64 - self.before as i64
}
}
/// A before→after artifact diff (from the warehouse's `artifact_diff`): total size
/// change, per-section deltas (the waterfall steps), the symbol churn buckets, and
/// an ABI-changed flag.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactDiff {
/// Total artifact bytes before.
pub before_bytes: u64,
/// Total artifact bytes after.
pub after_bytes: u64,
/// Per-section deltas — the waterfall steps.
pub sections: Vec<SectionDelta>,
/// Symbols present only after (added).
pub added: Vec<String>,
/// Symbols present only before (removed).
pub removed: Vec<String>,
/// Symbols that grew.
pub grown: Vec<SymbolDelta>,
/// Symbols that shrank.
pub shrunk: Vec<SymbolDelta>,
/// Whether the public ABI changed.
pub abi_changed: bool,
/// A short human note about the ABI verdict.
pub abi_note: String,
}
impl ArtifactDiff {
/// Signed total byte delta (`after - before`).
pub fn total_delta(&self) -> i64 {
self.after_bytes as i64 - self.before_bytes as i64
}
}
// ─────────────────────────────────────────────────────────────────────────────
// The seam — implemented by the HOST (nornir/dwarves), which feeds rows from the
// warehouse. No method reaches for a backend from the pane's side.
// ─────────────────────────────────────────────────────────────────────────────
/// The binary-analysis **data seam** the [`BinaryPane`] renders — implemented by the
/// **HOST** (nornir/dwarves), which feeds the rows from the warehouse's
/// `binary_facts` / `section_sizes` / `symbol_sizes` / `artifact_diff`. The pane
/// never computes any of these; it only paints whatever the seam returns.
pub trait BinaryDataSeam {
/// An honest banner for *where* the analysis came from, e.g.
/// `"nornir warehouse"` or `"stub (demo)"`.
fn source(&self) -> &str {
"binary-analysis"
}
/// The analysed artifact's name (e.g. `"nornir"`).
fn artifact_name(&self) -> String;
/// The hardening verdict (`binary_facts`) → the checksec badges.
fn checksec(&self) -> Checksec;
/// Is [`checksec`](Self::checksec) a MEASUREMENT? A source that does not grade the
/// ELF — the Mímir v2 binary layer joins linked symbols, it does not read RELRO /
/// PIE / NX — answers `false`: the pane then draws NO badge (every flag off is not
/// "unhardened", it is "unmeasured") and its `state_json` carries `checksec: null`.
/// Defaulted `true`, so every existing seam is unchanged.
fn checksec_measured(&self) -> bool {
true
}
/// Per-crate bloat aggregates → the treemap's top-level tiles. The pane sorts
/// and lays these out; the host may return them in any order.
fn crate_sizes(&self) -> Vec<CrateSize>;
/// Per-symbol sizes → the treemap leaves / bloat detail. Defaulted empty (the
/// crate tiles alone make a valid treemap).
fn symbol_sizes(&self) -> Vec<SymbolSize> {
Vec::new()
}
/// The before→after diff, if a baseline artifact exists. `None` ⇒ the waterfall
/// layer is skipped.
fn diff(&self) -> Option<ArtifactDiff> {
None
}
/// The artifact's size across recent builds (oldest→newest bytes) → the
/// sparkline. Defaulted empty (the sparkline layer is then skipped).
fn size_history(&self) -> Vec<u64> {
Vec::new()
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Squarified treemap layout — a small self-contained helper (no facett rect/layout
// helper exists to reuse). Pure geometry, unit-testable, no `ui`.
// ─────────────────────────────────────────────────────────────────────────────
/// A laid-out treemap tile: which crate, its bytes, its rect, and its colour.
#[derive(Clone, Debug, PartialEq)]
pub struct CrateTile {
/// The crate this tile represents.
pub crate_name: String,
/// Bytes attributed to the crate.
pub bytes: u64,
/// The tile's rectangle inside the treemap area.
pub rect: Rect,
/// The tile's fill colour (stable per crate name).
pub color: Color32,
}
/// The worst (largest) aspect ratio in a candidate row laid along a strip of
/// length `length` — the squarify cost function (Bruls et al.).
fn worst_ratio(row: &[f64], length: f64) -> f64 {
if row.is_empty() || length <= 0.0 {
return f64::INFINITY;
}
let s: f64 = row.iter().sum();
if s <= 0.0 {
return f64::INFINITY;
}
let rmax = row.iter().cloned().fold(f64::MIN, f64::max);
let rmin = row.iter().cloned().fold(f64::MAX, f64::min);
let ll = length * length;
let ss = s * s;
(ll * rmax / ss).max(ss / (ll * rmin))
}
/// **Squarified treemap** — lay out `weights` (bytes) into `area`, returning one
/// rect per weight in the SAME order. Bigger weights get proportionally bigger
/// rects with near-square aspect ratios. Zero/negative weights collapse to
/// [`Rect::NOTHING`]; an empty/degenerate area returns all-`NOTHING`.
pub fn squarified_treemap(weights: &[f64], area: Rect) -> Vec<Rect> {
let n = weights.len();
let mut out = vec![Rect::NOTHING; n];
if n == 0 || area.width() <= 0.0 || area.height() <= 0.0 {
return out;
}
// Only positive weights participate; keep them ordered largest-first for a good
// squarification, but write results back to the caller's original index.
let mut order: Vec<usize> = (0..n).filter(|&i| weights[i] > 0.0).collect();
if order.is_empty() {
return out;
}
order.sort_by(|&a, &b| weights[b].partial_cmp(&weights[a]).unwrap_or(std::cmp::Ordering::Equal));
let total: f64 = order.iter().map(|&i| weights[i]).sum();
let area_px = area.width() as f64 * area.height() as f64;
// Pixel-area target for each participating weight, largest-first.
let areas: Vec<f64> = order.iter().map(|&i| weights[i] / total * area_px).collect();
// The shrinking free rectangle.
let mut fx = area.min.x as f64;
let mut fy = area.min.y as f64;
let mut fw = area.width() as f64;
let mut fh = area.height() as f64;
let mut k = 0usize;
while k < order.len() {
let side = fw.min(fh);
// Grow the row while the worst aspect ratio keeps improving.
let mut row_area = vec![areas[k]];
let mut row_len = 1usize;
let mut j = k + 1;
while j < order.len() {
let cur = worst_ratio(&row_area, side);
row_area.push(areas[j]);
let with = worst_ratio(&row_area, side);
if with <= cur {
row_len += 1;
j += 1;
} else {
row_area.pop();
break;
}
}
// Lay the finished row along the shorter side.
let row_sum: f64 = row_area.iter().sum();
let thickness = if side > 0.0 { row_sum / side } else { 0.0 };
if fw >= fh {
// Vertical column of width `thickness` at the left; items stacked in y.
let mut oy = fy;
for m in 0..row_len {
let cell_h = if thickness > 0.0 { row_area[m] / thickness } else { 0.0 };
out[order[k + m]] = Rect::from_min_size(
Pos2::new(fx as f32, oy as f32),
Vec2::new(thickness as f32, cell_h as f32),
);
oy += cell_h;
}
fx += thickness;
fw -= thickness;
} else {
// Horizontal row of height `thickness` at the top; items along x.
let mut ox = fx;
for m in 0..row_len {
let cell_w = if thickness > 0.0 { row_area[m] / thickness } else { 0.0 };
out[order[k + m]] = Rect::from_min_size(
Pos2::new(ox as f32, fy as f32),
Vec2::new(cell_w as f32, thickness as f32),
);
ox += cell_w;
}
fy += thickness;
fh -= thickness;
}
k += row_len;
}
out
}
/// A stable fill colour for a crate name — reuses [`Oklch`] (no twinned colour
/// maths): a golden-ratio hue hash at a fixed lightness/chroma so a crate always
/// paints the same hue and adjacent tiles read apart.
pub fn crate_color(name: &str) -> Color32 {
// FNV-1a over the bytes → a stable hue in [0, 360).
let mut h: u64 = 0xcbf29ce484222325;
for b in name.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
let hue = (h % 360) as f32;
Oklch::new(0.68, 0.14, hue).to_color32()
}
// ─────────────────────────────────────────────────────────────────────────────
// The pane.
// ─────────────────────────────────────────────────────────────────────────────
/// Fill colour for a growth (size increase) in the waterfall — a warm red.
const GROW_COLOR: Color32 = Color32::from_rgb(0xd9, 0x53, 0x4f);
/// Fill colour for a shrink (size decrease) in the waterfall — a cool green.
const SHRINK_COLOR: Color32 = Color32::from_rgb(0x3f, 0xa6, 0x60);
/// Anchor colour for the before/after totals — a neutral blue.
const ANCHOR_COLOR: Color32 = Color32::from_rgb(0x4f, 0x83, 0xd9);
/// The shared **binary-analysis pane** — a [`Facet`](facett_core::Facet) the host
/// mounts, driving its host-supplied [`BinaryDataSeam`]. Presentational: it caches a
/// snapshot of the seam's rows and paints the four layers; [`refresh`](Self::refresh)
/// re-pulls from the seam.
pub struct BinaryPane {
seam: Box<dyn BinaryDataSeam>,
title: String,
// Cached snapshot of the seam's rows (re-pulled on `refresh`).
artifact: String,
checksec: Checksec,
/// The seam's [`BinaryDataSeam::checksec_measured`]: `false` ⇒ no badge is drawn.
checksec_measured: bool,
crates: Vec<CrateSize>,
symbols: Vec<SymbolSize>,
diff: Option<ArtifactDiff>,
history: Vec<u64>,
}
impl BinaryPane {
/// Build the pane over a host-supplied [`BinaryDataSeam`], pulling an initial
/// snapshot of its rows.
pub fn new(seam: Box<dyn BinaryDataSeam>) -> Self {
let mut pane = Self {
seam,
title: "Binary analysis".to_string(),
artifact: String::new(),
checksec: Checksec::default(),
checksec_measured: true,
crates: Vec::new(),
symbols: Vec::new(),
diff: None,
history: Vec::new(),
};
pane.refresh();
pane
}
/// Override the pane heading (default `"Binary analysis"`).
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
/// Re-pull the whole snapshot from the seam (call when the host has fresh
/// warehouse rows).
pub fn refresh(&mut self) {
self.artifact = self.seam.artifact_name();
self.checksec = self.seam.checksec();
self.checksec_measured = self.seam.checksec_measured();
self.crates = self.seam.crate_sizes();
// Largest crate first — the treemap and the JSON both read this order.
self.crates.sort_by_key(|c| std::cmp::Reverse(c.bytes));
self.symbols = self.seam.symbol_sizes();
self.diff = self.seam.diff();
self.history = self.seam.size_history();
}
/// The cached checksec verdict (read-only).
pub fn checksec(&self) -> &Checksec {
&self.checksec
}
/// The cached per-crate aggregates, largest-first (read-only).
pub fn crate_sizes(&self) -> &[CrateSize] {
&self.crates
}
/// The cached diff, if any (read-only).
pub fn diff(&self) -> Option<&ArtifactDiff> {
self.diff.as_ref()
}
/// **Lay out the bloat treemap** into `area` — the exact tiles the pane paints
/// (same helper, so the robot test can assert the painted layout as data). Tiles
/// come back largest-first; empty when there are no positive crate sizes.
pub fn crate_treemap(&self, area: Rect) -> Vec<CrateTile> {
let weights: Vec<f64> = self.crates.iter().map(|c| c.bytes as f64).collect();
let rects = squarified_treemap(&weights, area);
self.crates
.iter()
.zip(rects)
.filter(|(_, r)| r.is_finite() && r.width() > 0.0 && r.height() > 0.0)
.map(|(c, rect)| CrateTile {
crate_name: c.crate_name.clone(),
bytes: c.bytes,
rect,
color: crate_color(&c.crate_name),
})
.collect()
}
// ── painters (pure egui, no wgpu) ──────────────────────────────────────────
fn paint_treemap(&self, ui: &mut egui::Ui) {
let tiles = {
// Allocate a square-ish area for the treemap.
let w = ui.available_width().min(460.0);
let (rect, _resp) = ui.allocate_exact_size(Vec2::new(w, 200.0), Sense::hover());
let tiles = self.crate_treemap(rect);
let painter = ui.painter();
painter.rect_filled(rect, 2.0, Color32::from_gray(20));
for t in &tiles {
painter.rect_filled(t.rect, 1.0, t.color);
painter.rect_stroke(
t.rect,
1.0,
Stroke::new(1.0_f32, Color32::from_black_alpha(120)),
egui::StrokeKind::Inside,
);
// Label the tile if it is roomy enough to read.
if t.rect.width() > 44.0 && t.rect.height() > 16.0 {
painter.text(
t.rect.left_top() + Vec2::new(4.0, 3.0),
egui::Align2::LEFT_TOP,
&t.crate_name,
FontId::proportional(11.0),
Color32::from_black_alpha(200),
);
}
}
tiles
};
// A textual legend under the map — real widgets so a robot/screen-reader sees
// each crate + its bytes (the treemap rects themselves are painter-only).
ui.horizontal_wrapped(|ui| {
for t in &tiles {
let (chip, _) = ui.allocate_exact_size(Vec2::splat(10.0), Sense::hover());
ui.painter().rect_filled(chip, 2.0, t.color);
ui.label(format!("{} · {}", t.crate_name, human_bytes(t.bytes)));
}
});
}
fn paint_checksec(&self, ui: &mut egui::Ui) {
if !self.checksec_measured {
ui.weak(format!("not measured — {} does not grade the ELF (RELRO · PIE · NX · canary)", self.seam.source()));
return;
}
ui.horizontal_wrapped(|ui| {
for b in self.checksec.badges() {
let (fg, bg) = if b.ok {
(Color32::from_rgb(0x10, 0x2a, 0x18), SHRINK_COLOR)
} else {
(Color32::from_rgb(0x2a, 0x10, 0x10), GROW_COLOR)
};
let text = format!("{} {}", b.label, if b.ok { "✓" } else { "✗" });
// A real, coloured widget so the badge text is in the a11y tree.
let resp = ui.add(
egui::Label::new(egui::RichText::new(text).color(fg).background_color(bg))
.sense(Sense::hover()),
);
resp.on_hover_text(format!("{}: {}", b.label, b.detail));
}
});
}
fn paint_sparkline(&self, ui: &mut egui::Ui) {
if self.history.len() < 2 {
return;
}
let (rect, _resp) =
ui.allocate_exact_size(Vec2::new(ui.available_width().min(460.0), 40.0), Sense::hover());
let painter = ui.painter();
painter.rect_filled(rect, 2.0, Color32::from_gray(18));
let lo = *self.history.iter().min().unwrap() as f32;
let hi = *self.history.iter().max().unwrap() as f32;
let span = (hi - lo).max(1.0);
let n = self.history.len();
let pts: Vec<Pos2> = self
.history
.iter()
.enumerate()
.map(|(i, &v)| {
let x = rect.left() + rect.width() * (i as f32 / (n - 1) as f32);
let y = rect.bottom() - (rect.height() - 6.0) * ((v as f32 - lo) / span) - 3.0;
Pos2::new(x, y)
})
.collect();
painter.add(egui::Shape::line(pts.clone(), Stroke::new(1.6_f32, ANCHOR_COLOR)));
if let Some(&last) = pts.last() {
painter.circle_filled(last, 2.5, ANCHOR_COLOR);
}
}
fn paint_waterfall(&self, ui: &mut egui::Ui, diff: &ArtifactDiff) {
let (rect, _resp) =
ui.allocate_exact_size(Vec2::new(ui.available_width().min(460.0), 150.0), Sense::hover());
let painter = ui.painter();
painter.rect_filled(rect, 2.0, Color32::from_gray(18));
// Bars: [before] + one per section delta (floating) + [after].
let n_bars = diff.sections.len() + 2;
if n_bars == 0 {
return;
}
// Vertical scale: the tallest cumulative level we reach.
let mut level = diff.before_bytes as f64;
let mut peak = diff.before_bytes.max(diff.after_bytes) as f64;
for s in &diff.sections {
level += s.delta() as f64;
peak = peak.max(level);
}
let peak = (peak * 1.08).max(1.0);
let base_y = rect.bottom() - 4.0;
let top_y = rect.top() + 4.0;
let usable_h = (base_y - top_y).max(1.0);
let y_of = |bytes: f64| base_y - (bytes / peak) as f32 * usable_h;
let slot_w = rect.width() / n_bars as f32;
let bar_w = (slot_w * 0.7).max(2.0);
let bar_x = |i: usize| rect.left() + slot_w * (i as f32 + 0.5) - bar_w / 2.0;
// before anchor (full height from base).
let before_rect =
Rect::from_min_max(Pos2::new(bar_x(0), y_of(diff.before_bytes as f64)), Pos2::new(bar_x(0) + bar_w, base_y));
painter.rect_filled(before_rect, 1.0, ANCHOR_COLOR);
// floating section deltas.
let mut running = diff.before_bytes as f64;
for (i, s) in diff.sections.iter().enumerate() {
let d = s.delta() as f64;
let start = running;
let end = running + d;
running = end;
let (top, bot) = if end >= start { (end, start) } else { (start, end) };
let x = bar_x(i + 1);
let seg = Rect::from_min_max(Pos2::new(x, y_of(top)), Pos2::new(x + bar_w, y_of(bot)));
// grow = red (up), shrink = green (down); guarantee a visible sliver.
let seg = if seg.height() < 2.0 {
Rect::from_min_max(seg.left_top(), Pos2::new(seg.right(), seg.top() + 2.0))
} else {
seg
};
let col = if d >= 0.0 { GROW_COLOR } else { SHRINK_COLOR };
painter.rect_filled(seg, 1.0, col);
}
// after anchor.
let after_rect = Rect::from_min_max(
Pos2::new(bar_x(n_bars - 1), y_of(diff.after_bytes as f64)),
Pos2::new(bar_x(n_bars - 1) + bar_w, base_y),
);
painter.rect_filled(after_rect, 1.0, ANCHOR_COLOR);
// A textual summary line (real widgets so the delta is a11y-observable).
let total = diff.total_delta();
let sign = if total >= 0 { "+" } else { "−" };
let color = if total >= 0 { GROW_COLOR } else { SHRINK_COLOR };
ui.horizontal(|ui| {
ui.label(format!(
"{} → {}",
human_bytes(diff.before_bytes),
human_bytes(diff.after_bytes)
));
ui.colored_label(color, format!("{sign}{}", human_bytes(total.unsigned_abs())));
if diff.abi_changed {
ui.colored_label(GROW_COLOR, "ABI changed");
} else {
ui.colored_label(SHRINK_COLOR, "ABI stable");
}
});
ui.weak(format!(
"symbols: +{} added, −{} removed, ▲{} grown, ▼{} shrunk",
diff.added.len(),
diff.removed.len(),
diff.grown.len(),
diff.shrunk.len()
));
}
}
impl Facet for BinaryPane {
fn title(&self) -> &str {
&self.title
}
fn ui(&mut self, ui: &mut egui::Ui) {
ui.heading(&self.title);
ui.horizontal(|ui| {
ui.strong(&self.artifact);
ui.weak(format!("via {}", self.seam.source()));
});
ui.separator();
ui.label("Hardening");
self.paint_checksec(ui);
ui.add_space(6.0);
ui.label("Size history");
self.paint_sparkline(ui);
ui.add_space(6.0);
ui.label("Bloat by crate");
self.paint_treemap(ui);
ui.add_space(6.0);
if let Some(diff) = self.diff.clone() {
ui.separator();
ui.label("Size diff vs baseline");
self.paint_waterfall(ui, &diff);
}
}
fn state_json(&self) -> serde_json::Value {
let tiles = self.crate_treemap(Rect::from_min_size(Pos2::ZERO, Vec2::new(400.0, 200.0)));
let mut o = serde_json::Map::new();
o.insert("kind".into(), serde_json::json!("binview"));
o.insert("source".into(), serde_json::json!(self.seam.source()));
o.insert("artifact".into(), serde_json::json!(self.artifact));
// Checksec as badges (label/ok/detail) — the render truth.
let badges: Vec<serde_json::Value> = self
.checksec
.badges()
.into_iter()
.map(|b| serde_json::json!({"label": b.label, "ok": b.ok, "detail": b.detail}))
.collect();
// An unmeasured verdict is `null`, never a row of red badges.
o.insert("checksec".into(), if self.checksec_measured { serde_json::json!(badges) } else { serde_json::Value::Null });
// The treemap tiles as data (crate/bytes/area) — largest-first.
let tj: Vec<serde_json::Value> = tiles
.iter()
.map(|t| {
serde_json::json!({
"crate": t.crate_name,
"bytes": t.bytes,
"area": (t.rect.width() * t.rect.height()) as f64,
})
})
.collect();
o.insert("treemap".into(), serde_json::json!(tj));
o.insert("crate_count".into(), serde_json::json!(self.crates.len()));
o.insert("history".into(), serde_json::json!(self.history));
if let Some(d) = &self.diff {
o.insert(
"diff".into(),
serde_json::json!({
"before_bytes": d.before_bytes,
"after_bytes": d.after_bytes,
"total_delta": d.total_delta(),
"sections": d.sections.iter().map(|s| serde_json::json!({
"name": s.name, "before": s.before, "after": s.after, "delta": s.delta(),
})).collect::<Vec<_>>(),
"added": d.added,
"removed": d.removed,
"grown": d.grown.iter().map(|s| serde_json::json!({
"symbol": s.symbol, "delta": s.delta(),
})).collect::<Vec<_>>(),
"shrunk": d.shrunk.iter().map(|s| serde_json::json!({
"symbol": s.symbol, "delta": s.delta(),
})).collect::<Vec<_>>(),
"abi_changed": d.abi_changed,
"abi_note": d.abi_note,
}),
);
}
serde_json::Value::Object(o)
}
fn update_json(&mut self, msg_json: &str) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json)
&& v.get("refresh").and_then(|t| t.as_bool()) == Some(true)
{
self.refresh();
}
}
fn kind(&self) -> &'static str {
"binview"
}
}
/// Format a byte count for a compact label (`12.3 KB`, `4.1 MB`).
pub fn human_bytes(bytes: u64) -> String {
const U: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
let mut v = bytes as f64;
let mut i = 0;
while v >= 1024.0 && i < U.len() - 1 {
v /= 1024.0;
i += 1;
}
if i == 0 {
format!("{bytes} {}", U[0])
} else {
format!("{v:.1} {}", U[i])
}
}
// ─────────────────────────────────────────────────────────────────────────────
// A network-free stub seam for the demo + the robot test.
// ─────────────────────────────────────────────────────────────────────────────
/// A **stub** [`BinaryDataSeam`] with a realistic-looking snapshot (a bloated
/// `nornir` binary) for the shipped demo + the robot test. Deterministic: the
/// biggest crate is `nornir`, the checksec verdict mixes green/red, and the diff
/// carries both a growth and a shrink.
pub struct StubBinaryData;
impl BinaryDataSeam for StubBinaryData {
fn source(&self) -> &str {
"stub (demo)"
}
fn artifact_name(&self) -> String {
"nornir".to_string()
}
fn checksec(&self) -> Checksec {
Checksec {
relro: Relro::Full, // green
pie: true, // green
nx: true, // green
stack_canary: false, // red
stripped: true, // green (size win)
fortify: false, // red
}
}
fn crate_sizes(&self) -> Vec<CrateSize> {
vec![
CrateSize::new("nornir", 1_940_000),
CrateSize::new("std", 820_000),
CrateSize::new("arrow", 610_000),
CrateSize::new("serde", 240_000),
CrateSize::new("tokio", 180_000),
CrateSize::new("[Unknown]", 96_000),
]
}
fn symbol_sizes(&self) -> Vec<SymbolSize> {
vec![
SymbolSize::new("nornir", "nornir::warehouse::Mimir::query", 84_200),
SymbolSize::new("nornir", "nornir::release::topo_sort", 51_100),
SymbolSize::new("arrow", "arrow::compute::kernels::sort", 42_800),
SymbolSize::new("std", "core::fmt::write", 12_400),
]
}
fn diff(&self) -> Option<ArtifactDiff> {
Some(ArtifactDiff {
before_bytes: 3_620_000,
after_bytes: 3_886_000,
sections: vec![
SectionDelta::new(".text", 2_400_000, 2_610_000), // +210k grown
SectionDelta::new(".rodata", 720_000, 690_000), // −30k shrunk
SectionDelta::new(".data", 180_000, 186_000), // +6k grown
SectionDelta::new(".bss", 320_000, 400_000), // +80k grown
],
added: vec![
"nornir::binview::render".to_string(),
"nornir::warehouse::binary_facts".to_string(),
],
removed: vec!["nornir::legacy::old_report".to_string()],
grown: vec![
SymbolDelta::new("nornir::warehouse::Mimir::query", 62_000, 84_200),
SymbolDelta::new("nornir::release::topo_sort", 40_000, 51_100),
],
shrunk: vec![SymbolDelta::new("arrow::compute::kernels::sort", 58_000, 42_800)],
abi_changed: true,
abi_note: "added 2 pub fns to nornir::binview".to_string(),
})
}
fn size_history(&self) -> Vec<u64> {
vec![3_120_000, 3_300_000, 3_620_000, 3_540_000, 3_886_000]
}
}
// ── testmatrix emit shim (real row emitter with `--features testmatrix`, a
// compiled-out no-op otherwise — the facett-auth-ui idiom) ──────────────────
/// Emit one functional-status row (no-op unless built `--features testmatrix`).
#[cfg(feature = "testmatrix")]
pub fn emit(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
/// No-op shape compiled when `testmatrix` is OFF (the release path).
#[cfg(not(feature = "testmatrix"))]
#[inline(always)]
pub fn emit(_component: &str, _check: &str, _ok: bool, _detail: &str) {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stub_snapshot_reads_through_state_json() {
let pane = BinaryPane::new(Box::new(StubBinaryData));
let s = pane.state_json();
assert_eq!(s["kind"], "binview");
assert_eq!(s["artifact"], "nornir");
// Treemap: largest-first, nornir the biggest tile, all areas positive.
let tm = s["treemap"].as_array().unwrap();
assert!(tm.len() >= 2);
assert_eq!(tm[0]["crate"], "nornir");
let a0 = tm[0]["area"].as_f64().unwrap();
let a1 = tm[1]["area"].as_f64().unwrap();
assert!(a0 > 0.0 && a1 > 0.0 && a0 >= a1, "biggest crate has the biggest area");
// Checksec: a green and a red both present.
let badges = s["checksec"].as_array().unwrap();
assert!(badges.iter().any(|b| b["label"] == "PIE" && b["ok"] == true));
assert!(badges.iter().any(|b| b["label"] == "CANARY" && b["ok"] == false));
// Diff: a growth and a shrink section both present.
let secs = s["diff"]["sections"].as_array().unwrap();
assert!(secs.iter().any(|d| d["delta"].as_i64().unwrap() > 0), "a grown section");
assert!(secs.iter().any(|d| d["delta"].as_i64().unwrap() < 0), "a shrunk section");
assert_eq!(s["diff"]["abi_changed"], true);
}
#[test]
fn treemap_biggest_crate_gets_biggest_rect() {
let pane = BinaryPane::new(Box::new(StubBinaryData));
let tiles = pane.crate_treemap(Rect::from_min_size(Pos2::ZERO, Vec2::new(400.0, 200.0)));
assert!(!tiles.is_empty());
// Coverage is non-blank: the tiles fill most of the area.
let area: f32 = tiles.iter().map(|t| t.rect.width() * t.rect.height()).sum();
assert!(area > 400.0 * 200.0 * 0.7, "treemap covers the area (non-blank)");
// The biggest-bytes crate has the biggest rect.
let biggest = tiles.iter().max_by_key(|t| t.bytes).unwrap();
let widest = tiles
.iter()
.max_by(|a, b| {
(a.rect.width() * a.rect.height())
.partial_cmp(&(b.rect.width() * b.rect.height()))
.unwrap()
})
.unwrap();
assert_eq!(biggest.crate_name, widest.crate_name);
assert_eq!(biggest.crate_name, "nornir");
}
#[test]
fn squarified_treemap_is_proportional_and_bounded() {
let area = Rect::from_min_size(Pos2::ZERO, Vec2::new(300.0, 200.0));
let rects = squarified_treemap(&[100.0, 50.0, 25.0, 25.0], area);
// Every rect stays inside the area.
for r in &rects {
assert!(area.contains_rect(r.shrink(0.01)), "tile within bounds: {r:?}");
}
// The 100-weight rect is bigger than the 50-weight rect.
let a: f32 = rects[0].width() * rects[0].height();
let b: f32 = rects[1].width() * rects[1].height();
assert!(a > b);
// Zero weights collapse to NOTHING.
let z = squarified_treemap(&[1.0, 0.0], area);
assert!(!z[1].is_finite() || z[1].area() == 0.0);
}
/// A seam that does not grade the ELF draws no badge: `checksec` is `null` in the
/// state, not six red ✗ (all flags off is "unmeasured", not "unhardened").
#[test]
fn an_unmeasured_checksec_is_null_not_red_badges() {
struct Symbols;
impl BinaryDataSeam for Symbols {
fn artifact_name(&self) -> String {
"a".to_string()
}
fn checksec(&self) -> Checksec {
Checksec::default()
}
fn checksec_measured(&self) -> bool {
false
}
fn crate_sizes(&self) -> Vec<CrateSize> {
vec![CrateSize { crate_name: "a".into(), bytes: 10 }]
}
}
let s = BinaryPane::new(Box::new(Symbols)).state_json();
assert!(s["checksec"].is_null(), "{s}");
assert_eq!(s["crate_count"], 1);
// and a seam that does not say keeps its badges
assert!(BinaryPane::new(Box::new(StubBinaryData)).state_json()["checksec"].is_array());
}
#[test]
fn empty_seam_does_not_panic() {
struct Empty;
impl BinaryDataSeam for Empty {
fn artifact_name(&self) -> String {
"empty".to_string()
}
fn checksec(&self) -> Checksec {
Checksec::default()
}
fn crate_sizes(&self) -> Vec<CrateSize> {
Vec::new()
}
}
let pane = BinaryPane::new(Box::new(Empty));
let s = pane.state_json();
assert_eq!(s["crate_count"], 0);
assert!(pane.crate_treemap(Rect::from_min_size(Pos2::ZERO, Vec2::new(10.0, 10.0))).is_empty());
assert!(pane.diff().is_none());
}
}