use crate::primitives::hash::{sha256, sha256d};
use crate::primitives::{from_base58, to_base58};
use crate::Error;
use crate::Result;
pub const UHRP_PREFIX: &str = "uhrp://";
pub const WEB_UHRP_PREFIX: &str = "web+uhrp://";
const UHRP_VERSION_PREFIX: [u8; 2] = [0xce, 0x00];
const MIN_HASH_LENGTH: usize = 32;
pub fn normalize_url(url: &str) -> String {
let lower = url.to_lowercase();
if lower.starts_with(WEB_UHRP_PREFIX) {
url[WEB_UHRP_PREFIX.len()..].to_string()
} else if lower.starts_with(UHRP_PREFIX) {
url[UHRP_PREFIX.len()..].to_string()
} else if lower.starts_with("uhrp:") {
let rest = &url[5..];
if let Some(stripped) = rest.strip_prefix("//") {
stripped.to_string()
} else {
rest.to_string()
}
} else {
url.to_string()
}
}
pub fn get_url_for_hash(hash: &[u8]) -> Result<String> {
if hash.len() != MIN_HASH_LENGTH {
return Err(Error::InvalidDataLength {
expected: MIN_HASH_LENGTH,
actual: hash.len(),
});
}
let mut payload = Vec::with_capacity(2 + 32 + 4);
payload.extend_from_slice(&UHRP_VERSION_PREFIX);
payload.extend_from_slice(hash);
let checksum = sha256d(&payload);
payload.extend_from_slice(&checksum[..4]);
let encoded = to_base58(&payload);
Ok(format!("{}{}", UHRP_PREFIX, encoded))
}
pub fn get_url_for_file(data: &[u8]) -> Result<String> {
let hash = sha256(data);
get_url_for_hash(&hash)
}
pub fn get_hash_from_url(url: &str) -> Result<[u8; 32]> {
let normalized = normalize_url(url);
let decoded = from_base58(&normalized)?;
if decoded.len() < 38 {
return Err(Error::InvalidDataLength {
expected: 38,
actual: decoded.len(),
});
}
let prefix = &decoded[..2];
let hash = &decoded[2..34];
let checksum = &decoded[34..38];
if prefix != UHRP_VERSION_PREFIX {
return Err(Error::InvalidBase58(format!(
"Bad prefix: expected {:02x}{:02x}, got {:02x}{:02x}",
UHRP_VERSION_PREFIX[0], UHRP_VERSION_PREFIX[1], prefix[0], prefix[1]
)));
}
let data_for_checksum = &decoded[..34]; let expected_checksum = sha256d(data_for_checksum);
if checksum != &expected_checksum[..4] {
return Err(Error::InvalidChecksum);
}
let mut result = [0u8; 32];
result.copy_from_slice(hash);
Ok(result)
}
pub fn is_valid_url(url: &str) -> bool {
get_hash_from_url(url).is_ok()
}
pub fn get_hash_hex_from_url(url: &str) -> Result<String> {
let hash = get_hash_from_url(url)?;
Ok(hex::encode(hash))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::hash::sha256;
#[test]
fn test_normalize_url_with_uhrp_prefix() {
let url = "uhrp://abc123";
assert_eq!(normalize_url(url), "abc123");
}
#[test]
fn test_normalize_url_with_web_prefix() {
let url = "web+uhrp://abc123";
assert_eq!(normalize_url(url), "abc123");
}
#[test]
fn test_normalize_url_without_prefix() {
let url = "abc123";
assert_eq!(normalize_url(url), "abc123");
}
#[test]
fn test_normalize_url_case_insensitive() {
let url = "UHRP://ABC123";
assert_eq!(normalize_url(url), "ABC123");
}
#[test]
fn test_get_url_for_hash_valid() {
let hash = sha256(b"hello world");
let url = get_url_for_hash(&hash).unwrap();
assert!(url.starts_with(UHRP_PREFIX));
}
#[test]
fn test_get_url_for_hash_invalid_length() {
let short_hash = vec![0u8; 16];
let result = get_url_for_hash(&short_hash);
assert!(result.is_err());
}
#[test]
fn test_get_url_for_file() {
let data = b"hello world";
let url = get_url_for_file(data).unwrap();
assert!(url.starts_with(UHRP_PREFIX));
let hash = get_hash_from_url(&url).unwrap();
assert_eq!(hash, sha256(data));
}
#[test]
fn test_get_url_for_empty_file() {
let data = b"";
let url = get_url_for_file(data).unwrap();
assert!(url.starts_with(UHRP_PREFIX));
let hash = get_hash_from_url(&url).unwrap();
let expected = sha256(b"");
assert_eq!(hash, expected);
}
#[test]
fn test_roundtrip() {
let data = b"test file content";
let url = get_url_for_file(data).unwrap();
let hash = get_hash_from_url(&url).unwrap();
assert_eq!(hash, sha256(data));
}
#[test]
fn test_get_hash_from_url_invalid_prefix() {
let hash = sha256(b"test");
let mut payload = Vec::new();
payload.extend_from_slice(&[0x00, 0x00]); payload.extend_from_slice(&hash);
let checksum = sha256d(&payload);
payload.extend_from_slice(&checksum[..4]);
let encoded = to_base58(&payload);
let url = format!("{}{}", UHRP_PREFIX, encoded);
let result = get_hash_from_url(&url);
assert!(result.is_err());
}
#[test]
fn test_get_hash_from_url_invalid_checksum() {
let hash = sha256(b"test");
let mut payload = Vec::new();
payload.extend_from_slice(&UHRP_VERSION_PREFIX);
payload.extend_from_slice(&hash);
payload.extend_from_slice(&[0, 0, 0, 0]); let encoded = to_base58(&payload);
let url = format!("{}{}", UHRP_PREFIX, encoded);
let result = get_hash_from_url(&url);
assert!(matches!(result, Err(Error::InvalidChecksum)));
}
#[test]
fn test_get_hash_from_url_too_short() {
let url = "uhrp://short";
let result = get_hash_from_url(url);
assert!(result.is_err());
}
#[test]
fn test_is_valid_url() {
let valid_url = get_url_for_file(b"test").unwrap();
assert!(is_valid_url(&valid_url));
assert!(!is_valid_url("https://example.com"));
assert!(!is_valid_url("uhrp://tooshort"));
assert!(!is_valid_url("invalid"));
}
#[test]
fn test_get_hash_hex_from_url() {
let data = b"hello";
let url = get_url_for_file(data).unwrap();
let hash_hex = get_hash_hex_from_url(&url).unwrap();
assert_eq!(hash_hex.len(), 64);
assert_eq!(hash_hex, hex::encode(sha256(data)));
}
#[test]
fn test_cross_sdk_compatibility() {
let empty_hash = sha256(b"");
assert_eq!(
hex::encode(empty_hash),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
let url = get_url_for_hash(&empty_hash).unwrap();
let recovered = get_hash_from_url(&url).unwrap();
assert_eq!(recovered, empty_hash);
}
#[test]
fn test_url_with_web_prefix_roundtrip() {
let data = b"test";
let url = get_url_for_file(data).unwrap();
let web_url = url.replace("uhrp://", "web+uhrp://");
let hash = get_hash_from_url(&web_url).unwrap();
assert_eq!(hash, sha256(data));
}
const TS_EXAMPLE_HASH_HEX: &str =
"1a5ec49a3f32cd56d19732e89bde5d81755ddc0fd8515dc8b226d47654139dca";
const TS_EXAMPLE_FILE_HEX: &str =
"687da27f04a112aa48f1cab2e7949f1eea4f7ba28319c1e999910cd561a634a05a3516e6db";
const TS_EXAMPLE_URL_BASE58: &str = "XUT6PqWb3GP3LR7dmBMCJwZ3oo5g1iGCF3CrpzyuJCemkGu1WGoq";
#[test]
fn test_cross_sdk_vector_hash_to_url() {
let hash = hex::decode(TS_EXAMPLE_HASH_HEX).unwrap();
let mut hash_array = [0u8; 32];
hash_array.copy_from_slice(&hash);
let url = get_url_for_hash(&hash_array).unwrap();
let normalized = normalize_url(&url);
assert_eq!(normalized, TS_EXAMPLE_URL_BASE58);
}
#[test]
fn test_cross_sdk_vector_file_to_url() {
let file = hex::decode(TS_EXAMPLE_FILE_HEX).unwrap();
let url = get_url_for_file(&file).unwrap();
let normalized = normalize_url(&url);
assert_eq!(normalized, TS_EXAMPLE_URL_BASE58);
}
#[test]
fn test_cross_sdk_vector_url_to_hash() {
let hash = get_hash_from_url(TS_EXAMPLE_URL_BASE58).unwrap();
assert_eq!(hex::encode(hash), TS_EXAMPLE_HASH_HEX);
let url_with_prefix = format!("uhrp://{}", TS_EXAMPLE_URL_BASE58);
let hash2 = get_hash_from_url(&url_with_prefix).unwrap();
assert_eq!(hex::encode(hash2), TS_EXAMPLE_HASH_HEX);
}
#[test]
fn test_cross_sdk_vector_file_hash_matches() {
let file = hex::decode(TS_EXAMPLE_FILE_HEX).unwrap();
let computed_hash = sha256(&file);
assert_eq!(hex::encode(computed_hash), TS_EXAMPLE_HASH_HEX);
}
#[test]
fn test_cross_sdk_vector_is_valid() {
assert!(is_valid_url(TS_EXAMPLE_URL_BASE58));
assert!(is_valid_url(&format!("uhrp://{}", TS_EXAMPLE_URL_BASE58)));
assert!(is_valid_url(&format!(
"web+uhrp://{}",
TS_EXAMPLE_URL_BASE58
)));
let bad_url = "XUU7cTfy6fA6q2neLDmzPqJnGB6o18PXKoGaWLPrH1SeWLKgdCKq";
assert!(!is_valid_url(bad_url));
}
}