use std::borrow::Cow;
fn is_dangerous_format_char(c: char) -> bool {
matches!(
c as u32,
0x200E | 0x200F | 0x202A..=0x202E | 0x2066..=0x2069 | 0x200B..=0x200D |
0xFEFF )
}
#[must_use]
pub fn sanitize_display(input: &str, max_len: usize) -> String {
let mut stripped = String::with_capacity(input.len());
let mut in_escape = false;
for c in input.chars() {
if c == '\x1b' {
in_escape = true;
continue;
}
if in_escape {
if c.is_ascii_alphabetic() {
in_escape = false;
}
continue;
}
stripped.push(c);
}
stripped
.chars()
.filter(|c| (!c.is_control() || *c == ' ') && !is_dangerous_format_char(*c))
.take(max_len)
.collect()
}
#[must_use]
pub fn truncate_chars(s: &str, max_chars: usize) -> &str {
match s.char_indices().nth(max_chars) {
Some((end, _)) => &s[..end],
None => s,
}
}
#[must_use]
pub fn truncate_did(did: &str, max_len: usize) -> Cow<'_, str> {
if did.char_indices().nth(max_len).is_none() {
Cow::Borrowed(did)
} else if max_len > 3 {
Cow::Owned(format!("{}...", truncate_chars(did, max_len - 3)))
} else {
Cow::Owned(truncate_chars(did, max_len).to_string())
}
}
#[must_use]
pub fn truncate_did_centered(did: &str, max_len: usize) -> Cow<'_, str> {
let char_count = did.chars().count();
if char_count <= max_len {
return Cow::Borrowed(did);
}
let ellipsis = "...";
let keep = (max_len.saturating_sub(ellipsis.len())) / 2;
let start: String = did.chars().take(keep).collect();
let end: String = did.chars().skip(char_count - keep).collect();
Cow::Owned(format!("{start}...{end}"))
}
#[must_use]
pub fn shorten_webvh(did: &str, scid_keep: usize) -> Cow<'_, str> {
let Ok(parsed) = didwebvh_rs::url::WebVHURL::parse_did_url(did) else {
return Cow::Borrowed(did);
};
let scid = &parsed.scid;
if scid.chars().count() <= scid_keep * 2 + 1 {
return Cow::Borrowed(did);
}
let head: String = scid.chars().take(scid_keep).collect();
let tail: String = scid
.chars()
.skip(scid.chars().count() - scid_keep)
.collect();
Cow::Owned(did.replacen(scid.as_str(), &format!("{head}…{tail}"), 1))
}
#[must_use]
pub fn shorten_for_display(did: &str, max_len: usize) -> Cow<'_, str> {
let short = shorten_webvh(did, 4);
let count = short.chars().count();
if count <= max_len {
return short;
}
if max_len < 8 {
return Cow::Owned(truncate_did(&short, max_len).into_owned());
}
let head_len = (max_len / 4).min(10);
let tail_len = max_len - head_len - 1;
let head: String = short.chars().take(head_len).collect();
let tail: String = short.chars().skip(count - tail_len).collect();
let head = head.trim_end_matches(ELLIPSIS);
let tail = tail.trim_start_matches(ELLIPSIS);
Cow::Owned(format!("{head}{ELLIPSIS}{tail}"))
}
const ELLIPSIS: char = '\u{2026}';
#[must_use]
pub fn display_identifier<'a>(name: Option<&'a str>, did: &'a str, max_len: usize) -> Cow<'a, str> {
match name {
Some(name) => truncate_did(name, max_len),
None => shorten_for_display(did, max_len),
}
}
#[must_use]
pub fn task_label(type_uri: &str) -> String {
if let Some((_, tail)) = type_uri.split_once("/spec/")
&& !tail.is_empty()
{
return tail.to_string();
}
let segments: Vec<&str> = type_uri.rsplit('/').take(2).collect();
match segments.len() {
0 => type_uri.to_string(),
1 => segments[0].to_string(),
_ => format!("{}/{}", segments[1], segments[0]),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_trust_task_label_names_the_task_not_its_version() {
assert_eq!(
task_label("https://trusttasks.org/spec/credential-exchange/issue/0.1"),
"credential-exchange/issue/0.1"
);
assert_eq!(
task_label("https://trusttasks.org/spec/keys/create/0.1"),
"keys/create/0.1"
);
}
#[test]
fn a_legacy_didcomm_label_keeps_its_action() {
assert_eq!(
task_label("https://didcomm.org/trust-ping/2.0/ping"),
"2.0/ping"
);
}
#[test]
fn a_degenerate_type_still_produces_something() {
assert_eq!(task_label("bare"), "bare");
assert_eq!(task_label(""), "");
assert!(!task_label("https://trusttasks.org/spec/").is_empty());
}
#[test]
fn truncate_did_passes_through_short_input() {
let short = "did:web:example.com";
let out = truncate_did(short, 60);
assert_eq!(out, short);
assert!(matches!(out, Cow::Borrowed(_)));
}
#[test]
fn truncate_did_appends_ellipsis() {
let long = "did:webvh:abcdef0123456789:example.com:custom:path:here";
let out = truncate_did(long, 20);
assert_eq!(out.len(), 20);
assert!(out.ends_with("..."));
}
#[test]
fn truncate_did_below_ellipsis_width_returns_raw_truncation() {
let out = truncate_did("did:web:example.com", 2);
assert_eq!(out, "di");
}
#[test]
fn truncators_cut_on_char_boundaries() {
for scalar in ['\u{0281}', '\u{20AC}', '\u{1F600}'] {
let s = format!("{}{scalar}{}", "x".repeat(19), "y".repeat(40));
for max in 0..25 {
let out = truncate_did(&s, max);
assert!(out.chars().count() <= max.max(3), "max {max}: over budget");
assert!(truncate_chars(&s, max).chars().count() <= max);
}
}
}
#[test]
fn truncate_chars_counts_characters_not_bytes() {
assert_eq!(truncate_chars("😀😀😀", 2), "😀😀");
assert_eq!(truncate_chars("abc", 10), "abc");
assert_eq!(truncate_chars("abc", 0), "");
}
#[test]
fn truncate_did_centered_passes_through_short() {
let short = "did:web:x.io";
let out = truncate_did_centered(short, 60);
assert_eq!(out, short);
assert!(matches!(out, Cow::Borrowed(_)));
}
#[test]
fn truncate_did_centered_keeps_both_ends() {
let long = "did:webvh:abcdef0123456789:example.com:custom:path";
let out = truncate_did_centered(long, 20);
assert!(out.starts_with("did:web"));
assert!(out.contains("..."));
assert!(out.ends_with("path"));
}
#[test]
fn shortening_a_webvh_keeps_the_host_and_path() {
let did =
"did:webvh:QmUFMgMsbGpei5bGSx6yXT8ZTkmKUT7sQMGWFkx28aFtL3:webvh.storm.ws:kid-long";
let short = shorten_webvh(did, 4);
assert_eq!(short, "did:webvh:QmUF\u{2026}FtL3:webvh.storm.ws:kid-long");
assert!(short.ends_with("kid-long"), "the path survives");
assert!(short.contains("webvh.storm.ws"), "and so does the host");
assert!(short.chars().count() < did.chars().count());
}
#[test]
fn shortening_leaves_other_identifiers_alone() {
for other in [
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"example.com/@alice",
"not a did at all",
] {
assert_eq!(shorten_webvh(other, 4), other);
}
}
#[test]
fn display_identifier_prefers_the_name() {
let did = "did:webvh:abcdef0123456789:example.com:alice";
assert_eq!(
display_identifier(Some("example.com/@alice"), did, 40),
"example.com/@alice"
);
}
#[test]
fn display_identifier_falls_back_to_a_did_that_keeps_its_path() {
let did = "did:webvh:QmZBWtbz1VVTwyFNUHjx8YfL9fZ8XDc6KrBQ66tj9xq6Eh:dids.ic3.dev:alice";
let out = display_identifier(None, did, 30);
assert!(out.starts_with("did:web"), "{out}");
assert!(out.ends_with(":alice"), "{out}");
assert!(out.chars().count() <= 30, "{out}");
}
#[test]
fn shortening_for_display_keeps_two_dids_on_one_host_apart() {
let host = "QmZBWtbz1VVTwyFNUHjx8YfL9fZ8XDc6KrBQ66tj9xq6Eh:dids.ic3.dev";
let a = format!("did:webvh:{host}:firstperson-vtc");
let b = format!("did:webvh:{host}:earn-auction");
for width in [24, 32, 48, 64] {
let (sa, sb) = (
shorten_for_display(&a, width),
shorten_for_display(&b, width),
);
assert_ne!(sa, sb, "width {width}");
assert!(sa.ends_with("firstperson-vtc"), "width {width}: {sa}");
assert!(sb.ends_with("earn-auction"), "width {width}: {sb}");
assert!(sa.chars().count() <= width, "width {width}: {sa}");
}
}
#[test]
fn shortening_for_display_survives_anything() {
for s in [
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"example.com/@alice",
"",
"😀😀😀😀😀😀😀😀😀😀😀😀",
] {
for width in 0..20 {
let out = shorten_for_display(s, width);
assert!(out.chars().count() <= width.max(3), "{s:?} at {width}");
}
}
}
#[test]
fn display_identifier_truncates_an_overlong_name() {
let did = "did:web:x";
let out = display_identifier(
Some("verylonghost.example.com/@a-very-long-handle"),
did,
20,
);
assert_eq!(out.len(), 20);
assert!(out.ends_with("..."));
}
}