use base64::{engine::general_purpose::URL_SAFE as BASE_64, Engine};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree};
use syn::__private::ToTokens;
use syn::{punctuated::Punctuated, Ident, LitStr, Token};
#[cfg(not(any(feature = "blake3", feature = "sha2", feature = "md5",)))]
compile_error!("At least one hasher feature must be enabled");
#[proc_macro_attribute]
pub fn breaking(args: TokenStream, input: TokenStream) -> TokenStream {
let args =
syn::parse_macro_input!(args with Punctuated::<TokenTree, Token![=]>::parse_terminated);
let Some(expected) = args.iter().find_map(|arg| {
let TokenTree::Literal(lit) = arg else {
return None;
};
syn::parse::<LitStr>(lit.to_token_stream().into()).ok()
}) else {
let msg = "couldn't find string literal of hash in arguments";
let err = syn::Error::new(Span::call_site(), msg);
return err.into_compile_error().into();
};
let expected = expected.value();
let hasher = args.iter().find_map(|arg| {
let TokenTree::Ident(ident) = arg else {
return None;
};
Some(ident)
});
let input_str = sanitize(input.to_string());
#[cfg(feature = "print_token_stream")]
{
eprintln!(
"\
TokenStream string: {:?}
sanitized: {:?}\
",
input.to_string(),
crate::sanitize(input.to_string())
);
}
#[cfg(not(all(feature = "blake3", feature = "sha2", feature = "md5",)))]
let feature_error = |feature: &str| {
let msg = format!(
"either the `{feature}` feature must be enabled or a different hasher must be specified"
);
let err = syn::Error::new_spanned(hasher.unwrap(), msg);
TokenStream::from(err.into_compile_error())
};
let hash = match hasher.map(Ident::to_string).as_deref() {
None | Some("blake3") => {
#[cfg(feature = "blake3")]
{
blake3::hash(input_str.as_bytes()).as_bytes().to_vec()
}
#[cfg(not(feature = "blake3"))]
return feature_error("blake3");
}
Some(s) if s.starts_with("sha") => {
#[cfg(feature = "sha2")]
{
use sha2::Digest;
fn sha2(input: &str, mut hasher: impl Digest) -> Vec<u8> {
hasher.update(input.as_bytes());
hasher.finalize().to_vec()
}
match s {
"sha2" | "sha256" => sha2(&input_str, sha2::Sha256::new()),
"sha224" => sha2(&input_str, sha2::Sha224::new()),
"sha384" => sha2(&input_str, sha2::Sha384::new()),
"sha512" => sha2(&input_str, sha2::Sha512::new()),
"sha512_224" => sha2(&input_str, sha2::Sha512_224::new()),
"sha512_256" => sha2(&input_str, sha2::Sha512_256::new()),
_ => {
return syn::Error::new_spanned(
hasher.unwrap(),
"unrecognized sha version",
)
.into_compile_error()
.into();
}
}
}
#[cfg(not(feature = "sha2"))]
return feature_error("sha2");
}
Some("md5") => {
#[cfg(feature = "md5")]
{
md5::compute(input_str.as_bytes()).0.to_vec()
}
#[cfg(not(feature = "md5"))]
return feature_error("md5");
}
Some(other) => {
let msg = format!("unrecognized hasher `{other}`");
return syn::Error::new_spanned(hasher.unwrap(), msg)
.into_compile_error()
.into();
}
};
let hash = BASE_64.encode(hash);
if hash != expected {
let msg = format!(
"\
hash {hash:?} doesn't match.
TokenStream string: {input_str:?}
unsanitized: {:?}\
",
input.to_string()
);
return syn::Error::new(proc_macro::Span::call_site().into(), msg)
.into_compile_error()
.into();
}
input
}
fn sanitize(s: String) -> String {
let mut in_quote = false;
let mut raw_quote_surround_stack = Vec::new();
let mut escaped = false;
s.chars()
.map(|c| {
if !in_quote {
if raw_quote_surround_stack.is_empty() {
if c == 'r' {
raw_quote_surround_stack.push(c);
escaped = false;
return c;
}
} else if c == '#' {
raw_quote_surround_stack.push(c);
escaped = false;
return c;
} else if c == '"' {
in_quote = true;
raw_quote_surround_stack.push(c);
escaped = false;
return c;
}
} else if c != 'r' && raw_quote_surround_stack.last() == Some(&'r') {
raw_quote_surround_stack.pop();
escaped = false;
return c;
}
if c == '"' {
if raw_quote_surround_stack.last() == Some(&'"') {
raw_quote_surround_stack.pop();
} else if !escaped {
in_quote = !in_quote;
}
}
if c == '#' && raw_quote_surround_stack.last() == Some(&'#') {
raw_quote_surround_stack.pop();
}
if c == '\\' && in_quote && raw_quote_surround_stack.is_empty() {
escaped = !escaped;
} else {
escaped = false;
}
let is_line_breaker = c == '\n' || c == '\r';
if in_quote || !is_line_breaker {
c
} else {
' '
}
})
.collect()
}