use super::*;
use crate::output::diff_table::{build_diff_sections, DiffSection, EMPTY_PLACEHOLDER};
impl HumanFormatter {
pub(super) fn format_diff(&self, diff: &crate::diff::DomainDiff) -> String {
let mut output = Vec::new();
let domain_a = sanitize_line(&diff.domain_a);
let domain_b = sanitize_line(&diff.domain_b);
output.push(self.header(&format!("Diff: {domain_a} vs {domain_b}")));
let sections = build_diff_sections(diff);
let col_width = compute_column_width(§ions, &domain_a, &domain_b);
let label_width = sections
.iter()
.flat_map(|s| s.rows.iter().map(|r| r.label.chars().count()))
.max()
.unwrap_or(0);
let label_indent = " "; let section_indent = " "; let marker_gutter_width = 2; let header_left_pad = label_indent.chars().count() + label_width + 2 + marker_gutter_width;
let header_a = elide_to(&domain_a, col_width);
let header_b = elide_to(&domain_b, col_width);
let header_line = format!(
"{}{} {}",
" ".repeat(header_left_pad),
self.label(&format!("{header_a:<col_width$}")),
self.label(&header_b)
);
let rule_a: String = "─".repeat(header_a.chars().count());
let rule_b: String = "─".repeat(header_b.chars().count());
let rule_line = format!(
"{}{} {}",
" ".repeat(header_left_pad),
self.label(&format!("{rule_a:<col_width$}")),
self.label(&rule_b)
);
output.push(String::new());
output.push(header_line);
output.push(rule_line);
for section in §ions {
output.push(String::new());
output.push(format!("{}{}", section_indent, self.label(section.title)));
for row in §ion.rows {
let wrap = |values: &[String]| -> Vec<String> {
values
.iter()
.flat_map(|v| wrap_cell(v, col_width))
.collect()
};
let mut a_lines = wrap(&row.a_values);
let mut b_lines = wrap(&row.b_values);
let rows_needed = a_lines.len().max(b_lines.len()).max(1);
a_lines.resize(rows_needed, String::new());
b_lines.resize(rows_needed, String::new());
let marker_glyph = if row.matches { "=" } else { "≠" };
let color = |s: &str| -> String {
if row.matches {
self.success(s)
} else {
self.error(s)
}
};
for (i, (a, b)) in a_lines.iter().zip(b_lines.iter()).enumerate() {
let label = if i == 0 { row.label } else { "" };
let label_cell = format!("{label_indent}{label:<label_width$}");
let marker_cell = if i == 0 {
format!("{} ", color(marker_glyph))
} else {
" ".to_string()
};
let color_value = |s: &str, raw: &str| -> String {
if raw.trim() == EMPTY_PLACEHOLDER {
self.dim(s)
} else {
color(s)
}
};
let a_cell = color_value(&format!("{a:<col_width$}"), a);
let b_cell = color_value(b, b);
output.push(format!(
"{} {}{} {}",
self.label(&label_cell),
marker_cell,
a_cell,
b_cell
));
}
}
}
if !diff.errors.is_empty() {
output.push(String::new());
for error in &diff.errors {
output.push(format!(" {} {}", self.warning("!"), sanitize_line(error)));
}
}
output.join("\n")
}
}
fn wrap_cell(text: &str, max_width: usize) -> Vec<String> {
let width = max_width.max(1);
let text = sanitize_line(text);
let chars: Vec<char> = text.chars().collect();
if chars.len() <= width {
return vec![text];
}
let mut out = Vec::new();
let mut i = 0;
while i < chars.len() {
let remaining = chars.len() - i;
if remaining <= width {
out.push(chars[i..].iter().collect());
break;
}
let window_end = i + width;
let break_at = (i..window_end).rev().find(|&k| chars[k].is_whitespace());
match break_at {
Some(k) if k > i => {
out.push(chars[i..k].iter().collect());
i = k + 1; }
_ => {
out.push(chars[i..window_end].iter().collect());
i = window_end;
}
}
}
out
}
const DIFF_COLUMN_CAP: usize = 40;
fn compute_column_width(sections: &[DiffSection], domain_a: &str, domain_b: &str) -> usize {
let mut widest = domain_a.chars().count().max(domain_b.chars().count());
for section in sections {
for row in §ion.rows {
for v in row.a_values.iter().chain(row.b_values.iter()) {
widest = widest.max(v.chars().count());
}
}
}
widest.clamp(1, DIFF_COLUMN_CAP)
}
fn elide_to(text: &str, width: usize) -> String {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= width {
return text.to_string();
}
if width == 0 {
return String::new();
}
let keep = width - 1;
let mut out: String = chars[..keep].iter().collect();
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diff::{DnsDiff, DomainDiff, RegistrationDiff, SslDiff};
use crate::output::diff_table::tests::make_sample_diff;
use crate::output::diff_table::DiffRow;
#[test]
fn wrap_cell_short_returns_single_line() {
assert_eq!(wrap_cell("hello", 10), vec!["hello".to_string()]);
}
#[test]
fn wrap_cell_wraps_at_word_boundary() {
let out = wrap_cell("the quick brown fox", 10);
assert_eq!(out, vec!["the quick".to_string(), "brown fox".to_string()]);
}
#[test]
fn wrap_cell_hard_breaks_when_no_whitespace() {
let out = wrap_cell("a.very.long.nameserver.example", 10);
assert_eq!(
out,
vec![
"a.very.lon".to_string(),
"g.nameserv".to_string(),
"er.example".to_string(),
]
);
}
#[test]
fn wrap_cell_exact_width_no_wrap() {
assert_eq!(wrap_cell("1234567890", 10), vec!["1234567890".to_string()]);
}
#[test]
fn wrap_cell_empty_input_returns_one_empty_line() {
assert_eq!(wrap_cell("", 10), vec!["".to_string()]);
}
#[test]
fn wrap_cell_zero_width_treated_as_one() {
let out = wrap_cell("abc", 0);
assert_eq!(out, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
}
#[test]
fn wrap_cell_flattens_newline_cr_tab() {
assert_eq!(wrap_cell("a\nb\tc\rd", 40), vec!["a b cd".to_string()]);
for line in wrap_cell("x\ny\nz", 3) {
assert!(!line.contains(['\n', '\r', '\t']), "{line:?}");
}
}
#[test]
fn format_diff_cell_newline_cannot_forge_rows() {
let mut diff = make_sample_diff();
diff.registration.registrar = (
Some("IANA\n Organization = Forged".to_string()),
Some("MarkMonitor".to_string()),
);
let out = diff_formatter().format_diff(&diff);
let org_rows = out
.lines()
.filter(|l| l.trim_start().starts_with("Organization"))
.count();
assert_eq!(org_rows, 1, "forged row appeared:\n{out}");
assert!(!out.contains('\t'), "tab leaked:\n{out}");
}
#[test]
fn compute_column_width_uses_widest_value_across_sections() {
let sections = vec![DiffSection {
title: "Registration",
rows: vec![
DiffRow {
label: "Registrar",
a_values: vec!["IANA".to_string()],
b_values: vec!["MarkMonitor".to_string()],
matches: false,
list: false,
},
DiffRow {
label: "Organization",
a_values: vec!["—".to_string()],
b_values: vec!["Google LLC".to_string()],
matches: false,
list: false,
},
],
}];
assert_eq!(compute_column_width(§ions, "a.com", "b.com"), 11);
}
#[test]
fn compute_column_width_respects_domain_width() {
let sections = vec![DiffSection {
title: "Registration",
rows: vec![DiffRow {
label: "Registrar",
a_values: vec!["x".to_string()],
b_values: vec!["y".to_string()],
matches: false,
list: false,
}],
}];
let w = compute_column_width(§ions, "very-long-domain.example", "b.com");
assert_eq!(w, "very-long-domain.example".chars().count());
}
#[test]
fn compute_column_width_caps_at_40() {
let long_value = "x".repeat(100);
let sections = vec![DiffSection {
title: "Registration",
rows: vec![DiffRow {
label: "Registrar",
a_values: vec![long_value],
b_values: vec!["y".to_string()],
matches: false,
list: false,
}],
}];
assert_eq!(compute_column_width(§ions, "a.com", "b.com"), 40);
}
#[test]
fn compute_column_width_minimum_sensible_default() {
let sections = vec![DiffSection {
title: "Registration",
rows: vec![DiffRow {
label: "X",
a_values: vec!["a".to_string()],
b_values: vec!["b".to_string()],
matches: true,
list: false,
}],
}];
let w = compute_column_width(§ions, "a", "b");
assert!(w >= 1);
}
fn diff_formatter() -> HumanFormatter {
HumanFormatter::new().without_colors()
}
#[test]
fn format_diff_shows_column_headers_with_domain_names() {
let f = diff_formatter();
let out = f.format_diff(&make_sample_diff());
assert!(
out.contains("example.com"),
"missing domain_a in output:\n{}",
out
);
assert!(
out.contains("google.com"),
"missing domain_b in output:\n{}",
out
);
assert!(out.contains("──"), "missing header underline:\n{}", out);
}
#[test]
fn format_diff_marks_differing_rows_with_neq() {
let f = diff_formatter();
let out = f.format_diff(&make_sample_diff());
let registrar_line = out
.lines()
.find(|l| l.contains("Registrar"))
.expect("registrar line missing");
assert!(
registrar_line.contains("≠"),
"registrar row should be marked differ: {}",
registrar_line
);
}
#[test]
fn format_diff_marks_matching_rows_with_eq() {
let f = diff_formatter();
let out = f.format_diff(&make_sample_diff());
let resolves_line = out
.lines()
.find(|l| l.contains("Resolves"))
.expect("resolves line missing");
assert!(
resolves_line.contains('='),
"resolves row should be marked match: {}",
resolves_line
);
assert!(!resolves_line.contains('≠'));
}
#[test]
fn format_diff_nameservers_reversed_order_is_match() {
let f = diff_formatter();
let out = f.format_diff(&make_sample_diff());
let ns_line = out
.lines()
.find(|l| l.contains("Nameservers"))
.expect("nameservers line missing");
assert!(
ns_line.contains('=') && !ns_line.contains('≠'),
"nameservers row should match (set equality): {}",
ns_line
);
}
#[test]
fn format_diff_organization_none_renders_em_dash() {
let f = diff_formatter();
let out = f.format_diff(&make_sample_diff());
let org_line = out
.lines()
.find(|l| l.contains("Organization"))
.expect("organization line missing");
assert!(org_line.contains("—"), "expected em dash: {}", org_line);
}
#[test]
fn format_diff_multi_value_a_records_one_per_line() {
let mut diff = make_sample_diff();
diff.dns.a_records = (
vec!["1.1.1.1".to_string(), "2.2.2.2".to_string()],
vec!["3.3.3.3".to_string(), "4.4.4.4".to_string()],
);
let f = diff_formatter();
let out = f.format_diff(&diff);
assert!(out.contains("1.1.1.1"), "missing 1.1.1.1:\n{}", out);
assert!(out.contains("2.2.2.2"), "missing 2.2.2.2:\n{}", out);
assert!(out.contains("3.3.3.3"), "missing 3.3.3.3:\n{}", out);
assert!(out.contains("4.4.4.4"), "missing 4.4.4.4:\n{}", out);
let a_records_label_count = out.matches("A Records").count();
assert_eq!(
a_records_label_count, 1,
"A Records label should appear exactly once:\n{}",
out
);
}
#[test]
fn format_diff_wraps_long_scalar_values() {
let mut diff = make_sample_diff();
let long = "a".repeat(60);
diff.ssl.issuer = (Some(long.clone()), Some("short".to_string()));
let f = diff_formatter();
let out = f.format_diff(&diff);
for line in out.lines() {
assert!(
!line.contains(&long),
"unwrapped 60-char value on one line: {}",
line
);
}
let chars_present = out.matches('a').count();
assert!(
chars_present >= 60,
"wrapped value should preserve all chars, got {}",
chars_present
);
}
#[test]
fn format_diff_plain_mode_contains_marker_glyphs() {
let out = diff_formatter().format_diff(&make_sample_diff());
assert!(out.contains('='), "plain mode missing =");
assert!(out.contains('≠'), "plain mode missing ≠");
assert!(out.contains('─'), "plain mode missing header rule ─");
}
#[test]
fn format_diff_all_matching_has_no_neq() {
let diff = DomainDiff {
domain_a: "a.com".to_string(),
domain_b: "a.com".to_string(),
registration: RegistrationDiff {
registrar: (Some("X".to_string()), Some("X".to_string())),
organization: (Some("Org".to_string()), Some("Org".to_string())),
created: (Some("2020".to_string()), Some("2020".to_string())),
expires: (Some("2030".to_string()), Some("2030".to_string())),
},
dns: DnsDiff {
a_records: (vec!["1.1.1.1".to_string()], vec!["1.1.1.1".to_string()]),
nameservers: (vec!["ns".to_string()], vec!["ns".to_string()]),
resolves: (Some(true), Some(true)),
},
ssl: SslDiff {
issuer: (Some("I".to_string()), Some("I".to_string())),
valid_until: (Some("2030".to_string()), Some("2030".to_string())),
days_remaining: (Some(10), Some(10)),
is_valid: (Some(true), Some(true)),
},
errors: Vec::new(),
};
let out = diff_formatter().format_diff(&diff);
assert!(
!out.contains('≠'),
"all-match diff should have no ≠:\n{}",
out
);
}
#[test]
fn format_diff_all_differing_has_no_eq() {
let diff = DomainDiff {
domain_a: "a.com".to_string(),
domain_b: "b.com".to_string(),
registration: RegistrationDiff {
registrar: (Some("X".to_string()), Some("Y".to_string())),
organization: (Some("OrgX".to_string()), Some("OrgY".to_string())),
created: (Some("2020".to_string()), Some("2021".to_string())),
expires: (Some("2030".to_string()), Some("2031".to_string())),
},
dns: DnsDiff {
a_records: (vec!["1.1.1.1".to_string()], vec!["2.2.2.2".to_string()]),
nameservers: (vec!["nsa".to_string()], vec!["nsb".to_string()]),
resolves: (Some(true), Some(false)),
},
ssl: SslDiff {
issuer: (Some("IA".to_string()), Some("IB".to_string())),
valid_until: (Some("2030".to_string()), Some("2031".to_string())),
days_remaining: (Some(10), Some(20)),
is_valid: (Some(true), Some(false)),
},
errors: Vec::new(),
};
let out = diff_formatter().format_diff(&diff);
for line in out.lines() {
if line.starts_with(" ") && line.len() > 10 {
assert!(
!line.contains('='),
"all-differing diff should have no = on field rows: {}",
line
);
}
}
}
#[test]
fn format_diff_uneven_list_lengths_pad_shorter_side() {
let mut diff = make_sample_diff();
diff.dns.nameservers = (
vec!["ns1".to_string(), "ns2".to_string(), "ns3".to_string()],
vec!["only".to_string()],
);
let out = diff_formatter().format_diff(&diff);
assert!(out.contains("ns1"), "ns1 missing:\n{}", out);
assert!(out.contains("ns2"), "ns2 missing:\n{}", out);
assert!(out.contains("ns3"), "ns3 missing:\n{}", out);
assert!(out.contains("only"), "right-side 'only' missing:\n{}", out);
assert_eq!(
out.matches("only").count(),
1,
"right-side value must appear exactly once:\n{}",
out
);
}
#[test]
fn elide_to_truncates_with_ellipsis_and_respects_width() {
assert_eq!(elide_to("short", 40), "short");
assert_eq!(elide_to("exactlyten", 10), "exactlyten");
let long = "a".repeat(50);
let out = elide_to(&long, 40);
assert_eq!(out.chars().count(), 40, "elided width must equal the cap");
assert!(out.ends_with('…'), "elision marker expected: {out}");
}
#[test]
fn format_diff_header_aligns_when_domain_exceeds_column_cap() {
let mut diff = make_sample_diff();
let long_domain = format!("{}.example.com", "x".repeat(50)); diff.domain_a = long_domain.clone();
let f = diff_formatter();
let out = f.format_diff(&diff);
for line in out.lines() {
if line.starts_with("Diff:") {
continue;
}
assert!(
!line.contains(&long_domain),
"untruncated long domain leaked onto an aligned line: {line}"
);
}
let cap = DIFF_COLUMN_CAP;
let has_capped_rule = out
.lines()
.any(|l| l.contains(&"─".repeat(cap)) && !l.contains(&"─".repeat(cap + 1)));
assert!(
has_capped_rule,
"expected an A-column rule capped at {cap} dashes:\n{out}"
);
}
#[test]
fn format_diff_em_dash_is_dim_not_row_color() {
colored::control::set_override(true);
let f = HumanFormatter::new();
let out = f.format_diff(&make_sample_diff());
colored::control::unset_override();
let org_line = out
.lines()
.find(|l| l.contains("Organization"))
.expect("organization line missing");
assert!(
!org_line.contains("\x1b[91m—"),
"em-dash should not be red on a differ row: {:?}",
org_line
);
assert!(
org_line.contains("\x1b[90m"),
"em-dash should be dim (bright-black ANSI): {:?}",
org_line
);
}
}