use std::borrow::Cow;
use crate::ObjectId;
fn keywords(bytes: &[u8]) -> impl Iterator<Item = std::ops::Range<usize>> + '_ {
let mut at = 0;
std::iter::from_fn(move || {
while let Some(found) = find(bytes.get(at..)?, b"$Id") {
let start = at + found;
let after = start + 3;
match bytes.get(after) {
Some(b'$') => {
at = after + 1;
return Some(start..at);
}
Some(b':') => {
let line = bytes.get(after + 1..).unwrap_or_default();
let close = line.iter().position(|&byte| byte == b'$' || byte == b'\n');
match close {
Some(close) if line.get(close) == Some(&b'$') => {
at = after + 1 + close + 1;
return Some(start..at);
}
_ => at = after,
}
}
_ => at = after,
}
}
None
})
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn foreign(keyword: &[u8]) -> bool {
let text = keyword
.get(4..keyword.len().saturating_sub(1))
.unwrap_or_default();
let text = text.strip_prefix(b" ").unwrap_or(text);
let text = text.strip_suffix(b" ").unwrap_or(text);
text.contains(&b' ')
}
fn replace<'a>(bytes: Cow<'a, [u8]>, with: &[u8], collapsing: bool) -> Cow<'a, [u8]> {
let ranges: Vec<_> = keywords(&bytes)
.filter(|range| {
let keyword = bytes.get(range.clone()).unwrap_or_default();
if collapsing {
range.len() != 4
} else {
!foreign(keyword)
}
})
.collect();
if ranges.is_empty() {
return bytes;
}
let mut out = Vec::with_capacity(bytes.len() + ranges.len() * with.len());
let mut start = 0;
for range in ranges {
out.extend_from_slice(bytes.get(start..range.start).unwrap_or_default());
out.extend_from_slice(with);
start = range.end;
}
out.extend_from_slice(bytes.get(start..).unwrap_or_default());
Cow::Owned(out)
}
pub(super) fn collapse(bytes: Cow<'_, [u8]>) -> Cow<'_, [u8]> {
replace(bytes, b"$Id$", true)
}
pub(super) fn expand(bytes: Cow<'_, [u8]>, id: ObjectId) -> Cow<'_, [u8]> {
let with = format!("$Id: {id} $");
replace(bytes, with.as_bytes(), false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collapse_matches_git() {
let input = b"$Id$ x $Id: abc $ y $Id:$ z $Id: no\nclose $ w $Id:foo$ $ID$ $Id";
let expected = b"$Id$ x $Id$ y $Id$ z $Id: no\nclose $ w $Id$ $ID$ $Id";
assert_eq!(&*collapse(Cow::Borrowed(input)), expected);
}
#[test]
fn expand_keeps_foreign_keywords() {
let id: ObjectId = "deb0e62babfba8c6f140ad6cdf6684e95f4b949e".parse().unwrap();
for kept in [
&b"$Id: foreign words $"[..],
b"$Id: x y$",
b"$Id: x $",
b"$Id: x $",
] {
assert_eq!(&*expand(Cow::Borrowed(kept), id), kept);
}
for replaced in [&b"$Id: $"[..], b"$Id: a:b $", b"$Id:x $", b"$Id: x$"] {
assert_ne!(&*expand(Cow::Borrowed(replaced), id), replaced);
}
assert_eq!(&*collapse(Cow::Borrowed(b"$Id: foreign words $")), b"$Id$");
}
#[test]
fn expand_replaces_closed_keywords() {
let id: ObjectId = "deb0e62babfba8c6f140ad6cdf6684e95f4b949e".parse().unwrap();
let out = expand(Cow::Borrowed(b"A $Id: old $ B $Id:x\nC $Id$\n"), id);
assert_eq!(
&*out,
b"A $Id: deb0e62babfba8c6f140ad6cdf6684e95f4b949e $ B $Id:x\nC \
$Id: deb0e62babfba8c6f140ad6cdf6684e95f4b949e $\n"
);
}
}