use std::collections::HashMap;
pub fn normalize_hash(raw:&str) -> &str { raw.split('|').next().unwrap_or(raw).trim() }
pub fn is_valid_ccr_hash(h:&str) -> bool {
if h.len() < 8 {
return false;
}
let h = h.to_lowercase();
if let Some(stripped) = h.strip_prefix("i:") {
stripped.len() >= 6 && stripped.chars().all(|c| c.is_ascii_hexdigit())
} else {
h.len() >= 24 && h.chars().all(|c| c.is_ascii_hexdigit())
}
}
pub fn ccr_marker(
hash_val:&str,
ccr_type:&str,
size:usize,
preview:&str,
headroom_budget:Option<u32>,
meta:Option<&HashMap<String, String>>,
center:Option<&str>,
) -> String {
let mut safe = preview.replace('|', "-").replace(['\n', '\r'], " ").trim().to_string();
safe = safe.chars().filter(|c| *c >= ' ').collect();
if let Some(budget) = headroom_budget {
safe = if budget < 25 {
safe.chars().take(30).collect()
} else if budget < 50 {
safe.chars().take(60).collect()
} else if budget < 75 {
safe.chars().take(100).collect()
} else {
safe
};
}
let meta_str = if let Some(m) = meta {
let parts:Vec<String> = m
.iter()
.filter_map(|(k, v)| {
let sv = v.replace('|', "/").replace('\n', " ").trim().to_string();
if sv.is_empty() { None } else { Some(format!("{}={}", k, sv)) }
})
.collect();
let mut s = parts.join(";");
if s.len() > 300 {
s = format!("{}...", crate::struct_extract::floor_boundary(&s, 297));
}
s
} else {
String::new()
};
render_marker(&safe, ccr_type, &meta_str, center, hash_val, size)
}
fn render_marker(preview:&str, ccr_type:&str, meta:&str, center:Option<&str>, hash:&str, size:usize) -> String {
let center_str = center.unwrap_or(ccr_type);
let meta_part = if meta.is_empty() { String::new() } else { format!("\n[meta:{}]", meta) };
let preview_line = if is_self_bracketed_preview(preview) {
preview.to_string()
} else {
format!("[{}:{}]", center_str, preview)
};
format!("<<<CCR:{}|{}|{}>>>\n{}{}", hash, ccr_type, size, preview_line, meta_part)
}
fn is_self_bracketed_preview(preview:&str) -> bool {
let p = preview.trim();
if !p.starts_with('[') || !p.ends_with(']') {
return false;
}
let inner = &p[1..];
match inner.find(':') {
Some(colon) => {
let label = &inner[..colon];
!label.is_empty() && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
},
None => false,
}
}
pub fn parse_preview(marker_line:&str) -> Option<String> {
let start = marker_line.find('[')?;
let colon = marker_line[start..].find(':')?;
let end = marker_line.rfind(']')?;
if end > start + colon {
Some(marker_line[start + colon + 1..end].to_string())
} else {
None
}
}
static HASH_RE:std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"(?:<<<|\[|\u{2af7})CCR:([0-9a-fA-F:i]{6,64})(?:\|[^\]>\n]*?)?(?:\]|>>>|\u{2af8})").unwrap()
});
pub fn extract_hashes(text:&str) -> Vec<String> {
HASH_RE
.captures_iter(text)
.filter_map(|cap| cap.get(1))
.map(|m| m.as_str().to_string())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_hash_bare_is_unchanged() {
assert_eq!(normalize_hash("abc123"), "abc123");
}
#[test]
fn test_normalize_hash_strips_pipe_suffix() {
assert_eq!(normalize_hash("abc123|tool|1024"), "abc123");
}
#[test]
fn test_normalize_hash_trims_whitespace() {
assert_eq!(normalize_hash(" abc123 "), "abc123");
}
#[test]
fn test_normalize_hash_is_idempotent() {
let once = normalize_hash(" abc123|tool|1024 ");
assert_eq!(normalize_hash(once), once);
}
#[test]
fn test_valid_hash() {
assert!(is_valid_ccr_hash("abc123def456abc123def456abc123def456"));
assert!(is_valid_ccr_hash("i:abc123def456"));
assert!(!is_valid_ccr_hash("short"));
assert!(!is_valid_ccr_hash(""));
}
#[test]
fn test_marker_format() {
let m = ccr_marker(
"abc123def456abc123def456abc123def456",
"code_rust",
1234,
"[code_rust:3fns 42L]",
None,
None,
None,
);
assert!(m.contains("<<<CCR:abc123def456abc123def456abc123def456|code_rust|1234>>>"));
assert_eq!(m.matches("3fns 42L").count(), 1);
let mut lines = m.lines();
assert!(lines.next().unwrap().starts_with("<<<CCR:"));
let preview_line = lines.next().unwrap();
assert!(preview_line.starts_with('['));
assert_eq!(preview_line, "[code_rust:3fns 42L]");
}
#[test]
fn test_marker_preview_never_doubles_bracket_prefix() {
let re = regex::Regex::new(r"\[\w+:\[").unwrap();
for ty in [
"text",
"git",
"ls",
"test",
"grep",
"gitlog",
"build",
"diff",
"code_rust",
"json_array",
] {
let preview = crate::build_preview(ty, "M crates/aphrodite/src/preview.rs\nA src/new.rs\n?? tmp\n");
let m = ccr_marker("abc123def456abc123def456abc123def456", ty, 42, &preview, None, None, None);
assert!(
!re.is_match(&m),
"marker preview must not double the bracket prefix for type {ty:?}: {m:?}"
);
}
}
#[test]
fn test_render_marker_wraps_bare_preview_but_not_bracketed() {
let bare = ccr_marker(
"abc123def456abc123def456abc123def456",
"text",
5,
"hello world",
None,
None,
None,
);
assert!(bare.contains("\n[text:hello world]"));
let bracketed = ccr_marker(
"abc123def456abc123def456abc123def456",
"git",
5,
"[git:2M | a.rs]",
None,
None,
None,
);
assert!(bracketed.contains("\n[git:2M - a.rs]"));
assert!(!bracketed.contains("[git:[git:"));
}
#[test]
fn test_marker_with_budget() {
let preview = "a very long preview string that should be truncated under tight budget constraints";
let m = ccr_marker(
"abc123def456abc123def456abc123def456",
"text",
100,
preview,
Some(20),
None,
None,
);
let preview_line = m.lines().nth(1).unwrap();
let inner = preview_line.split(':').nth(1).unwrap().trim_end_matches(']');
assert!(inner.len() <= 32); }
#[test]
fn test_extract_hashes() {
let text = "<<<CCR:aaa111|code|100>>>\nsome text\n<<<CCR:bbb222|diff|200>>>";
let hashes = extract_hashes(text);
assert_eq!(hashes, vec!["aaa111", "bbb222"]);
}
#[test]
fn test_is_valid_ccr_hash_boundary_band() {
assert!(!is_valid_ccr_hash("abcdef12")); assert!(!is_valid_ccr_hash("abcdef0123456789abcdef")); assert!(!is_valid_ccr_hash("abcdef0123456789abcdeff")); assert!(is_valid_ccr_hash("abcdef0123456789abcdef01")); }
#[test]
fn test_is_valid_ccr_hash_i_prefix_variants() {
assert!(!is_valid_ccr_hash("i:xyz")); assert!(is_valid_ccr_hash("i:abc123")); assert!(!is_valid_ccr_hash("i:abc1")); }
#[test]
fn test_is_valid_ccr_hash_uppercase() {
assert!(is_valid_ccr_hash("ABCDEF0123456789ABCDEF01"));
}
#[test]
fn test_extract_hashes_bracket_form() {
let hashes = extract_hashes("[CCR:aaa111|code]");
assert_eq!(hashes, vec!["aaa111"]);
}
#[test]
fn test_extract_hashes_glyph_terminated_form() {
let text = "<<<CCR:ccc333|text\u{2af8}";
let hashes = extract_hashes(text);
assert_eq!(hashes, vec!["ccc333"]);
}
#[test]
fn test_extract_hashes_unterminated_no_match() {
assert!(extract_hashes("<<<CCR:no_terminator_here").is_empty());
}
#[test]
fn test_extract_hashes_full_glyph_delimited_form() {
let hashes = extract_hashes("\u{2af7}CCR:abc123\u{2af8}");
assert_eq!(hashes, vec!["abc123"]);
}
#[test]
fn test_extract_hashes_does_not_cross_newlines() {
let text = "<<<CCR:foo\nbar>>>";
assert!(extract_hashes(text).is_empty(), "must not capture a multi-line garbage hash");
}
#[test]
fn test_parse_preview_on_garbage() {
assert_eq!(parse_preview("no brackets here"), None);
assert_eq!(parse_preview("[nocolon]"), None);
assert_eq!(parse_preview("[code_rust:hello]"), Some("hello".to_string()));
}
#[test]
fn test_ccr_marker_strips_interior_nul_bytes() {
let preview = "before\0after";
let m = ccr_marker("abc123def456abc123def456abc123def456", "text", 12, preview, None, None, None);
assert!(!m.contains('\0'), "NUL byte must be stripped from the preview: {m:?}");
assert!(m.contains("beforeafter"));
}
#[test]
fn test_ccr_marker_truncates_multibyte_preview_on_char_boundary() {
let preview = "é".repeat(50);
let m = ccr_marker(
"abc123def456abc123def456abc123def456",
"text",
100,
&preview,
Some(10), None,
None,
);
let preview_line = m.lines().nth(1).unwrap();
let inner = parse_preview(preview_line).unwrap();
assert_eq!(inner.chars().count(), 30, "must truncate to exactly 30 chars, not 30 bytes");
}
#[test]
fn test_ccr_marker_preview_containing_literal_marker_syntax_does_not_confuse_extraction() {
let literal_marker_text = "example: <<<CCR:fake000|text|1>>>";
let m = ccr_marker(
"abc123def456abc123def456abc123def456",
"text",
999,
literal_marker_text,
None,
None,
None,
);
assert!(m.starts_with("<<<CCR:abc123def456abc123def456abc123def456|text|999>>>"));
assert!(
m.contains("fake000-text-1"),
"embedded literal text survives, pipes mangled: {m:?}"
);
assert!(
!m.contains("fake000|text|1"),
"the original pipe-delimited literal must not survive intact: {m:?}"
);
let hashes = extract_hashes(&m);
assert_eq!(hashes, vec!["abc123def456abc123def456abc123def456"]);
}
}