use std::{
borrow::Cow,
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use rustc_hash::FxHashSet as HashSet;
pub fn calc_hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
s.finish()
}
pub struct ExtractedHashPattern {
pub pattern: String,
pub len: Option<usize>,
}
pub fn extract_hash_pattern(pattern: &str, key: &str) -> Option<ExtractedHashPattern> {
let key_offset = key.len() - 1;
let start = pattern.find(&key[..key_offset])?;
let end = pattern[start + key_offset..].find(']')?;
let len = pattern[start + key_offset..start + key_offset + end]
.strip_prefix(':')
.and_then(|n| n.parse::<usize>().ok());
let pattern = &pattern[start..=start + key_offset + end];
Some(ExtractedHashPattern {
pattern: pattern.to_string(),
len,
})
}
pub fn include_hash(filename: &str, hashes: &HashSet<String>) -> bool {
hashes.iter().any(|hash| filename.contains(hash))
}
pub trait Replacer {
fn get(&mut self, dst: &mut String, hash_len: Option<usize>, need_base64: bool);
}
impl Replacer for &str {
#[inline]
fn get(&mut self, dst: &mut String, _: Option<usize>, _: bool) {
dst.push_str(self);
}
}
impl Replacer for &String {
#[inline]
fn get(&mut self, dst: &mut String, _: Option<usize>, _: bool) {
dst.push_str(self);
}
}
impl<F, S> Replacer for F
where
F: FnMut(Option<usize>, bool) -> S,
S: AsRef<str>,
{
#[inline]
fn get(&mut self, dst: &mut String, hash_len: Option<usize>, need_base64: bool) {
dst.push_str((*self)(hash_len, need_base64).as_ref())
}
}
fn replace_all_placeholder_impl<'a>(
pattern: &'a str,
with_extra: bool,
mut placeholder: &'a str,
mut replacer: impl Replacer,
) -> Cow<'a, str> {
let offset = placeholder.len() - 1;
if with_extra {
placeholder = &placeholder[..offset];
}
let mut iter = pattern.match_indices(placeholder).peekable();
if iter.peek().is_none() {
return Cow::Borrowed(pattern);
}
let mut last_end = 0;
let mut result = String::with_capacity(pattern.len());
for (start, _) in iter {
if start < last_end {
continue;
}
let start_offset = start + offset;
let (end, len, need_base64) = if with_extra {
let rest = &pattern[start_offset..];
let Some(end) = rest.find(']') else {
continue;
};
if end == 0 {
(start_offset, None, false)
} else {
let matched = &rest[1..end];
let mut configs = matched.split(':');
let mut len = None;
let mut need_base64 = false;
if let Some(digest_or_len) = configs.next() {
match digest_or_len.parse::<usize>() {
Ok(l) => len = Some(l),
Err(_) => {
if digest_or_len == "base64" {
need_base64 = true;
} else {
continue;
}
}
}
};
if need_base64 && let Some(l) = configs.next() {
match l.parse::<usize>() {
Ok(l) => len = Some(l),
Err(_) => continue,
}
}
if len.is_none() {
continue;
}
(start_offset + end, len, need_base64)
}
} else {
(start_offset, None, false)
};
result.push_str(&pattern[last_end..start]);
replacer.get(&mut result, len, need_base64);
last_end = end + 1;
}
if last_end < pattern.len() {
result.push_str(&pattern[last_end..]);
}
Cow::Owned(result)
}
pub trait ReplaceAllPlaceholder {
fn replace_all<'a>(&'a self, placeholder: &'a str, replacer: impl Replacer) -> Cow<'a, str>;
fn replace_all_with_len<'a>(
&'a self,
placeholder: &'a str,
replacer: impl Replacer,
) -> Cow<'a, str>;
}
impl ReplaceAllPlaceholder for str {
#[inline]
fn replace_all<'a>(&'a self, placeholder: &'a str, replacer: impl Replacer) -> Cow<'a, str> {
replace_all_placeholder_impl(self, false, placeholder, replacer)
}
#[inline]
fn replace_all_with_len<'a>(
&'a self,
placeholder: &'a str,
replacer: impl Replacer,
) -> Cow<'a, str> {
replace_all_placeholder_impl(self, true, placeholder, replacer)
}
}
#[test]
fn test_replace_all_placeholder() {
let result = "hello-[hash]-[hash_name]-[hash:1].js".replace_all("[hash]", "abc");
assert_eq!(result, "hello-abc-[hash_name]-[hash:1].js");
let result =
"hello-[hash]-[hash:-]-[hash_name]-[hash:1]-[hash:].js".replace_all_with_len("[hash]", "abc");
assert_eq!(result, "hello-abc-[hash:-]-[hash_name]-abc-[hash:].js");
let result = "hello-[hash]-[hash:5]-[hash_name]-[hash:o].js".replace_all_with_len(
"[hash]",
|len: Option<usize>, base64: bool| {
assert!(!base64);
&"abcdefgh"[..len.unwrap_or(8)]
},
);
assert_eq!(result, "hello-abcdefgh-abcde-[hash_name]-[hash:o].js");
let result =
"[hash:base64:4]".replace_all_with_len("[hash]", |len: Option<usize>, base64: bool| {
assert!(base64);
&"abcdefgh"[..len.unwrap_or(8)]
});
assert_eq!(result, "abcd");
}