use crate::fail;
use base64::Engine;
use regex::Regex;
use sha2::{Digest, Sha512};
use std::sync::OnceLock;
use swc_core::common::DUMMY_SP;
fn hash_token_regex() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(
r"(?i)\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*[a-z]*))?(?::(\d+))?\]",
)
.expect("hash interpolation regex must compile")
})
}
fn any_token_regex() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"\[[^\]]*\]").expect("any-token regex must compile"))
}
pub fn validate_pattern(pattern: &str) {
let hash_re = hash_token_regex();
for m in any_token_regex().find_iter(pattern) {
if !hash_re.is_match(m.as_str()) {
fail(
DUMMY_SP,
format!(
"idInterpolationPattern token `{}` is not supported. \
Only `[<hashType>:(hash|contenthash):<digestType>:<length>]` is implemented. \
If you need `[name]`/`[ext]`/`[path]` substitution, extend the plugin.",
m.as_str()
),
);
}
}
}
pub fn interpolate_pattern(pattern: &str, content: &str) -> String {
hash_token_regex()
.replace_all(pattern, |caps: ®ex::Captures| {
let hash_type = caps.get(1).map(|m| m.as_str()).unwrap_or("md5");
let digest_type = caps.get(2).map(|m| m.as_str()).unwrap_or("hex");
let length = caps
.get(3)
.and_then(|m| m.as_str().parse::<usize>().ok())
.unwrap_or(9999);
hash_digest(content, hash_type, digest_type, length)
})
.into_owned()
}
fn hash_digest(content: &str, hash_type: &str, digest_type: &str, length: usize) -> String {
let bytes: Vec<u8> = match hash_type {
"sha512" => {
let mut h = Sha512::new();
h.update(content.as_bytes());
h.finalize().to_vec()
}
other => fail(
DUMMY_SP,
format!(
"hash type `{}` is not supported. Default is `sha512`. \
If you need md5/sha1/sha256, add the appropriate crate and a branch in hash.rs.",
other
),
),
};
let encoded = match digest_type {
"base64" => base64::engine::general_purpose::STANDARD.encode(&bytes),
"hex" => hex_lower(&bytes),
other => fail(
DUMMY_SP,
format!(
"digest type `{}` is not supported. Default is `base64`; `hex` also works.",
other
),
),
};
encoded.chars().take(length).collect()
}
fn hex_lower(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{:02x}", b));
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha512_base64_6_matches_node() {
let id = interpolate_pattern("[sha512:contenthash:base64:6]", "Hello, {name}!");
assert_eq!(id, "tBFOH1");
}
#[test]
fn sha512_base64_6_with_description() {
let id = interpolate_pattern(
"[sha512:contenthash:base64:6]",
"Hello, {name}!#Greets the user by name",
);
assert_eq!(id, "LlYGNJ");
}
}