pub const WINDOW: usize = 120;
pub const KEY_CHARS: usize = 16;
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
#[must_use]
pub fn fingerprint(
issue_id: &str,
issue_number: u32,
body: &str,
line: &str,
token_col: usize,
) -> String {
let window = normalized_window(line, token_col);
let mut payload = String::with_capacity(issue_id.len() + window.len() + body.len() + 24);
payload.push_str(issue_id);
payload.push('\0');
payload.push_str(&issue_number.to_string());
payload.push('\0');
for lower in body.to_lowercase().chars() {
payload.push(lower);
}
payload.push('\0');
payload.push_str(&window);
format!(
"{:0width$x}",
fnv1a64(payload.as_bytes()),
width = KEY_CHARS
)
}
#[must_use]
pub fn normalize(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut pending_space = false;
for ch in text.chars() {
if ch.is_whitespace() {
pending_space = !out.is_empty();
continue;
}
if pending_space {
out.push(' ');
pending_space = false;
}
for lower in ch.to_lowercase() {
out.push(lower);
}
}
out
}
fn normalized_window(line: &str, token_col: usize) -> String {
let chars: Vec<char> = line.chars().collect();
let len = chars.len();
let col = token_col.min(len);
let start = col.saturating_sub(WINDOW);
let end = (col + WINDOW).min(len);
let slice: String = chars[start..end].iter().collect();
normalize(&slice)
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut hash = FNV_OFFSET;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
fn key(body: &str, line: &str) -> String {
fingerprint("1A2B3C4D", 42, body, line, line.find('@').unwrap_or(0))
}
#[test]
fn an_identical_line_yields_a_stable_key() {
let line = "blocked on @herbert-maschke's review";
assert_eq!(key("herbert-maschke", line), key("herbert-maschke", line));
assert_eq!(key("herbert-maschke", line).len(), KEY_CHARS);
}
#[test]
fn a_whitespace_or_case_only_change_keeps_the_key() {
let base = key("herbert-maschke", "blocked on @herbert-maschke's review");
assert_eq!(
base,
key("herbert-maschke", "Blocked ON @herbert-maschke's Review")
);
assert_eq!(
base,
key(
"herbert-maschke",
" blocked on @herbert-maschke's review "
)
);
}
#[test]
fn the_key_is_insensitive_to_the_body_casing() {
assert_eq!(
key("herbert-maschke", "cc @herbert-maschke"),
key("HERBERT-MASCHKE", "cc @herbert-maschke")
);
}
#[test]
fn inserting_a_mention_on_another_line_leaves_every_existing_key_unchanged() {
let before = "line one @alice here\nline two @bob there";
let after = "line one @alice here\ninserted @carol between\nline two @bob there";
let a = key("alice", before.lines().next().unwrap());
let b = key("bob", before.lines().nth(1).unwrap());
let a2 = key("alice", after.lines().next().unwrap());
let b2 = key("bob", after.lines().nth(2).unwrap());
assert_eq!(a, a2);
assert_eq!(b, b2);
}
#[test]
fn a_different_body_on_the_same_line_is_a_different_key() {
let line = "cc @alice and @bob";
assert_ne!(
fingerprint("ID", 1, "alice", line, line.find("@alice").unwrap()),
fingerprint("ID", 1, "bob", line, line.find("@bob").unwrap())
);
}
#[test]
fn a_different_issue_identity_is_a_different_key() {
let line = "cc @alice";
assert_ne!(
fingerprint("ID1", 1, "alice", line, 3),
fingerprint("ID2", 1, "alice", line, 3)
);
assert_ne!(
fingerprint("ID1", 1, "alice", line, 3),
fingerprint("ID1", 2, "alice", line, 3)
);
}
#[test]
fn normalize_collapses_case_and_whitespace() {
assert_eq!(normalize(" Blocked ON @Alice.\n"), "blocked on @alice.");
}
}