use crate::error::SxurlError;
use crate::types::UrlComponents;
use crate::core::hasher::ComponentHasher;
pub fn pack_sxurl(components: &UrlComponents) -> Result<[u8; 32], SxurlError> {
let tld_hash = ComponentHasher::hash_tld(&components.tld)?;
let domain_hash = ComponentHasher::hash_domain(&components.domain)?;
let subdomain_hash = if components.subdomain.is_empty() {
0
} else {
ComponentHasher::hash_subdomain(&components.subdomain)?
};
let path_hash = ComponentHasher::hash_path(&components.path)?;
let params_hash = if components.query.is_empty() {
0
} else {
ComponentHasher::hash_params(&components.query)?
};
let fragment_hash = if components.fragment.is_empty() {
0
} else {
ComponentHasher::hash_fragment(&components.fragment)?
};
let scheme_code = match components.scheme.as_str() {
"https" => 0u8,
"http" => 1u8,
"ftp" => 2u8,
_ => return Err(SxurlError::InvalidScheme),
};
let path_present = !components.path.is_empty() && components.path != "/";
let port_present = components.port != get_default_port(&components.scheme);
let flags =
(if !components.subdomain.is_empty() { 1u8 } else { 0u8 }) << 7 |
(if port_present { 1u8 } else { 0u8 }) << 6 |
(if path_present { 1u8 } else { 0u8 }) << 5 |
(if !components.query.is_empty() { 1u8 } else { 0u8 }) << 4 |
(if !components.fragment.is_empty() { 1u8 } else { 0u8 }) << 3;
let mut hex_string = String::with_capacity(64);
hex_string.push_str(&format!("{:01x}", scheme_code));
hex_string.push_str("0");
hex_string.push_str(&format!("{:07x}", tld_hash));
hex_string.push_str(&format!("{:015x}", domain_hash));
hex_string.push_str(&format!("{:08x}", subdomain_hash));
hex_string.push_str(&format!("{:02x}", flags));
hex_string.push_str(&format!("{:04x}", components.port));
hex_string.push_str(&format!("{:013x}", path_hash));
hex_string.push_str(&format!("{:08x}", params_hash));
hex_string.push_str(&format!("{:05x}", fragment_hash));
if hex_string.len() != 64 {
return Err(SxurlError::InternalError);
}
hex_to_sxurl(&hex_string)
}
pub fn pack_sxurl_with_hashes(
components: &UrlComponents,
tld_hash: u64,
domain_hash: u64,
subdomain_hash: u64,
path_hash: u64,
params_hash: u64,
fragment_hash: u64,
) -> Result<[u8; 32], SxurlError> {
let scheme_code = match components.scheme.as_str() {
"https" => 0u8,
"http" => 1u8,
"ftp" => 2u8,
_ => return Err(SxurlError::InvalidScheme),
};
let path_present = !components.path.is_empty() && components.path != "/";
let port_present = components.port != get_default_port(&components.scheme);
let flags =
(if !components.subdomain.is_empty() { 1u8 } else { 0u8 }) << 7 |
(if port_present { 1u8 } else { 0u8 }) << 6 |
(if path_present { 1u8 } else { 0u8 }) << 5 |
(if !components.query.is_empty() { 1u8 } else { 0u8 }) << 4 |
(if !components.fragment.is_empty() { 1u8 } else { 0u8 }) << 3;
let mut hex_string = String::with_capacity(64);
hex_string.push_str(&format!("{:01x}", scheme_code));
hex_string.push_str("0");
hex_string.push_str(&format!("{:07x}", tld_hash));
hex_string.push_str(&format!("{:015x}", domain_hash));
hex_string.push_str(&format!("{:08x}", subdomain_hash));
hex_string.push_str(&format!("{:02x}", flags));
hex_string.push_str(&format!("{:04x}", components.port));
hex_string.push_str(&format!("{:013x}", path_hash));
hex_string.push_str(&format!("{:08x}", params_hash));
hex_string.push_str(&format!("{:05x}", fragment_hash));
if hex_string.len() != 64 {
return Err(SxurlError::InternalError);
}
hex_to_sxurl(&hex_string)
}
fn get_default_port(scheme: &str) -> u16 {
match scheme {
"http" => 80,
"https" => 443,
"ftp" => 21,
_ => 0,
}
}
pub fn sxurl_to_hex(sxurl: &[u8; 32]) -> String {
hex::encode(sxurl)
}
pub fn hex_to_sxurl(hex_str: &str) -> Result<[u8; 32], SxurlError> {
if hex_str.len() != 64 {
return Err(SxurlError::InvalidLength);
}
let bytes = hex::decode(hex_str)
.map_err(|_| SxurlError::InvalidHexCharacter)?;
if bytes.len() != 32 {
return Err(SxurlError::InvalidLength);
}
let mut result = [0u8; 32];
result.copy_from_slice(&bytes);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sxurl_hex_conversion() {
let sxurl = [0u8; 32];
let hex = sxurl_to_hex(&sxurl);
assert_eq!(hex.len(), 64);
assert_eq!(hex, "0".repeat(64));
let parsed = hex_to_sxurl(&hex).unwrap();
assert_eq!(parsed, sxurl);
}
#[test]
fn test_pack_sxurl_basic() {
let components = UrlComponents::new(
"https".to_string(),
"com".to_string(),
"example".to_string(),
"".to_string(),
443,
"/".to_string(),
"".to_string(),
"".to_string(),
);
let result = pack_sxurl(&components);
assert!(result.is_ok());
let sxurl = result.unwrap();
assert_eq!(sxurl.len(), 32);
let hex = sxurl_to_hex(&sxurl);
assert_eq!(hex.len(), 64);
assert!(hex.starts_with("00"), "HTTPS v2 should start with '00', got: {}", &hex[0..2]);
println!("SXURL for https://example.com/: {}", hex);
}
#[test]
fn test_pack_sxurl_with_params() {
let components = UrlComponents::new(
"https".to_string(),
"com".to_string(),
"google".to_string(),
"".to_string(),
443,
"/search".to_string(),
"q=test".to_string(),
"".to_string(),
);
let result = pack_sxurl(&components);
assert!(result.is_ok());
let sxurl = result.unwrap();
let hex = sxurl_to_hex(&sxurl);
assert!(hex.starts_with("00"), "HTTPS v2 should start with '00', got: {}", &hex[0..2]);
println!("SXURL for https://google.com/search?q=test: {}", hex);
}
#[test]
fn test_v2_format_positions() {
let components = UrlComponents::new(
"http".to_string(), "com".to_string(), "example".to_string(), "api".to_string(), 8080, "/v1/users".to_string(), "limit=10".to_string(), "section1".to_string(), );
let result = pack_sxurl(&components);
assert!(result.is_ok());
let hex = sxurl_to_hex(&result.unwrap());
assert_eq!(hex.len(), 64);
println!("✓ v2 format test:");
println!(" Full SXURL: {}", hex);
println!(" Positions breakdown:");
println!(" [0:1] Scheme: {} (http=1)", &hex[0..1]);
println!(" [1:2] Reserved: {} (always 0)", &hex[1..2]);
println!(" [2:9] TLD: {} (28-bit)", &hex[2..9]);
println!(" [9:24] Domain: {} (60-bit)", &hex[9..24]);
println!(" [24:32] Subdomain: {} (32-bit)", &hex[24..32]);
println!(" [32:34] Flags: {} (8-bit)", &hex[32..34]);
println!(" [34:38] Port: {} (16-bit)", &hex[34..38]);
println!(" [38:51] Path: {} (52-bit)", &hex[38..51]);
println!(" [51:59] Query: {} (32-bit)", &hex[51..59]);
println!(" [59:64] Fragment: {} (20-bit)", &hex[59..64]);
assert_eq!(&hex[0..1], "1", "HTTP scheme should be 1");
assert_eq!(&hex[1..2], "0", "Reserved should be 0");
assert_eq!(&hex[34..38], "1f90", "Port 8080 should encode as 1f90");
let flags_hex = &hex[32..34];
let flags = u8::from_str_radix(flags_hex, 16).unwrap();
assert_eq!(flags & 0x80, 0x80, "Subdomain flag should be set");
assert_eq!(flags & 0x40, 0x40, "Port flag should be set");
assert_eq!(flags & 0x20, 0x20, "Path flag should be set");
assert_eq!(flags & 0x10, 0x10, "Params flag should be set");
assert_eq!(flags & 0x08, 0x08, "Fragment flag should be set");
}
}