const SHA_LEN: usize = 40;
#[must_use]
pub fn is_full_sha(s: &str) -> bool {
s.len() == SHA_LEN && s.bytes().all(|b| b.is_ascii_hexdigit())
}
#[must_use]
pub fn is_tag_shaped(s: &str) -> bool {
if is_full_sha(s) {
return false;
}
let stripped = s.strip_prefix(['v', 'V']).unwrap_or(s);
stripped.starts_with(|c: char| c.is_ascii_digit())
}
#[must_use]
pub fn match_v_prefix_style(current: &str, tag: &str) -> String {
let current_has_v = current.starts_with(['v', 'V']);
let tag_has_v = tag.starts_with(['v', 'V']);
match (current_has_v, tag_has_v) {
(true, false) => format!("v{tag}"),
(false, true) => tag[1..].to_string(),
_ => tag.to_string(),
}
}
pub struct CharOffsets {
byte_of_char: Vec<usize>,
}
impl CharOffsets {
#[must_use]
pub fn new(content: &str) -> Self {
let mut byte_of_char: Vec<usize> = content.char_indices().map(|(b, _)| b).collect();
byte_of_char.push(content.len());
Self { byte_of_char }
}
#[must_use]
pub fn byte_offset(&self, char_index: usize) -> usize {
self.byte_of_char
.get(char_index)
.copied()
.unwrap_or(*self.byte_of_char.last().unwrap_or(&0))
}
}
pub const MAX_FALLBACK_SCAN_BYTES: usize = 1024;
#[must_use]
pub fn locate_value_span(content: &str, search_from: usize, value: &str) -> Option<(usize, usize)> {
if value.is_empty() {
return Some((search_from, search_from));
}
let bytes = content.as_bytes();
if search_from + value.len() <= bytes.len()
&& &bytes[search_from..search_from + value.len()] == value.as_bytes()
{
return Some((search_from, search_from + value.len()));
}
let line_end = bytes[search_from..]
.iter()
.position(|&b| b == b'\n')
.map_or(bytes.len(), |p| search_from + p);
let scan_end = line_end.min(search_from.saturating_add(MAX_FALLBACK_SCAN_BYTES));
let haystack = &bytes[search_from..scan_end];
let needle = value.as_bytes();
haystack
.windows(needle.len())
.position(|w| w == needle)
.map(|rel| (search_from + rel, search_from + rel + needle.len()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_full_sha_accepts_and_rejects() {
assert!(is_full_sha(&"a".repeat(40)));
assert!(!is_full_sha(&"a".repeat(39)));
assert!(!is_full_sha(&"g".repeat(40)));
}
#[test]
fn test_is_tag_shaped() {
assert!(is_tag_shaped("v4"));
assert!(is_tag_shaped("4.2.0"));
assert!(!is_tag_shaped("main"));
assert!(!is_tag_shaped(&"a".repeat(40)));
}
#[test]
fn test_match_v_prefix_style() {
assert_eq!(match_v_prefix_style("v4", "5.0.0"), "v5.0.0");
assert_eq!(match_v_prefix_style("4", "v5.0.0"), "5.0.0");
assert_eq!(match_v_prefix_style("v4", "v5.0.0"), "v5.0.0");
assert_eq!(match_v_prefix_style("4", "5.0.0"), "5.0.0");
}
#[test]
fn test_locate_value_span_finds_value_within_fallback_bound() {
let content = "prefix xxxxx actions/checkout@v4 suffix";
let value = "actions/checkout@v4";
let (start, end) = locate_value_span(content, 0, value).unwrap();
assert_eq!(&content[start..end], value);
}
#[test]
fn test_locate_value_span_gives_up_beyond_fallback_bound_instead_of_hanging() {
let filler = "x".repeat(MAX_FALLBACK_SCAN_BYTES + 100);
let value = "actions/checkout@v4";
let content = format!("{filler}{value}");
assert_eq!(locate_value_span(&content, 0, value), None);
}
#[test]
fn test_locate_value_span_bounded_scan_stays_fast_on_a_huge_line() {
let filler = "y".repeat(6 * 1024 * 1024);
let value = "not-present-in-filler@v4";
let content = format!("{filler}\n");
let start = std::time::Instant::now();
let result = locate_value_span(&content, 0, value);
assert!(
start.elapsed() < std::time::Duration::from_secs(1),
"locate_value_span took {:?}, expected a bounded scan to finish in well under 1s",
start.elapsed()
);
assert_eq!(result, None);
}
#[test]
fn test_char_offsets_byte_offset_ascii() {
let offsets = CharOffsets::new("hello");
assert_eq!(offsets.byte_offset(0), 0);
assert_eq!(offsets.byte_offset(5), 5);
}
#[test]
fn test_char_offsets_byte_offset_multibyte() {
let content = "\u{3000}a";
let offsets = CharOffsets::new(content);
assert_eq!(offsets.byte_offset(1), 3);
}
}