use md5::{Digest, Md5};
#[must_use]
pub fn to_hex(bytes: &[u8]) -> String {
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len().saturating_mul(2));
for &b in bytes {
let hi = usize::from(b >> 4);
let lo = usize::from(b & 0x0F);
s.push(char::from(HEX_CHARS.get(hi).copied().unwrap_or(b'0')));
s.push(char::from(HEX_CHARS.get(lo).copied().unwrap_or(b'0')));
}
s
}
pub fn sign_request(
method: &str,
endpoint: &str,
params: &mut [(String, String)],
app_secret: &str,
) -> String {
params.sort_by(|a, b| a.0.cmp(&b.0));
let mut input = String::new();
input.push_str(method);
input.push_str(endpoint);
for (key, value) in params {
input.push_str(key);
input.push_str(value);
}
input.push_str(app_secret);
let hash = Md5::digest(input.as_bytes());
to_hex(&hash)
}
#[must_use]
pub fn sign_track_file_url(
format_id: i32,
track_id: i32,
timestamp: &str,
app_secret: &str,
) -> String {
let input = format!(
"trackgetFileUrlformat_id{format_id}intentstreamtrack_id{track_id}{timestamp}{app_secret}"
);
let hash = Md5::digest(input.as_bytes());
to_hex(&hash)
}
#[cfg(test)]
mod tests {
use {
anyhow::{Result, ensure},
md5::{Digest, Md5},
};
use crate::signing::{sign_request, sign_track_file_url, to_hex};
fn expected_hash(input: &str) -> String {
to_hex(&Md5::digest(input.as_bytes()))
}
#[test]
fn sign_request_produces_deterministic_hash() -> Result<()> {
let mut params = vec![
("b".to_string(), "2".to_string()),
("a".to_string(), "1".to_string()),
];
let hash = sign_request("GET", "/test", &mut params, "secret");
ensure!(hash == expected_hash("GET/testa1b2secret"), "hash mismatch");
Ok(())
}
#[test]
fn sign_request_sorts_params_alphabetically() -> Result<()> {
let mut params1 = vec![
("z".to_string(), "last".to_string()),
("a".to_string(), "first".to_string()),
("m".to_string(), "middle".to_string()),
];
let mut params2 = vec![
("a".to_string(), "first".to_string()),
("m".to_string(), "middle".to_string()),
("z".to_string(), "last".to_string()),
];
let hash1 = sign_request("GET", "/ep", &mut params1, "s");
let hash2 = sign_request("GET", "/ep", &mut params2, "s");
ensure!(
hash1 == hash2,
"hashes should match regardless of input order"
);
Ok(())
}
#[test]
fn sign_track_file_url_produces_correct_format() -> Result<()> {
let hash = sign_track_file_url(6, 12345, "1234567890", "mysecret");
let expected =
expected_hash("trackgetFileUrlformat_id6intentstreamtrack_id123451234567890mysecret");
ensure!(hash == expected, "hash mismatch");
Ok(())
}
#[test]
fn sign_request_empty_params() -> Result<()> {
let mut params: Vec<(String, String)> = vec![];
let hash = sign_request("POST", "/empty", &mut params, "key");
ensure!(hash == expected_hash("POST/emptykey"), "hash mismatch");
Ok(())
}
}