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) };
format!(
"<<<CCR:{}|{}|{}>>>\n[{}:{}]{}",
hash, ccr_type, size, center_str, preview, meta_part
)
}
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:"));
assert!(lines.next().unwrap().starts_with('['));
}
#[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()));
}
}