High-performance implementation of plcbundle written in Rust
at main 180 lines 5.5 kB view raw
1// Utility functions for validation and common operations 2 3use crate::constants; 4use axum::http::{HeaderMap, HeaderValue, Uri}; 5 6/// Check if a path is a common browser file that should be ignored 7pub fn is_common_browser_file(path: &str) -> bool { 8 let common_files = [ 9 "favicon.ico", 10 "robots.txt", 11 "sitemap.xml", 12 "apple-touch-icon.png", 13 ".well-known", 14 ]; 15 let common_extensions = [ 16 ".ico", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".css", ".js", ".woff", ".woff2", ".ttf", 17 ".eot", ".xml", ".txt", ".html", 18 ]; 19 20 for file in &common_files { 21 if path == *file || path.starts_with(file) { 22 return true; 23 } 24 } 25 26 for ext in &common_extensions { 27 if path.ends_with(ext) { 28 return true; 29 } 30 } 31 32 false 33} 34 35/// Validate if input is a valid DID or handle 36pub fn is_valid_did_or_handle(input: &str) -> bool { 37 if input.is_empty() { 38 return false; 39 } 40 41 // If it's a DID 42 if input.starts_with("did:") { 43 // Only accept did:plc: method 44 if !input.starts_with("did:plc:") { 45 return false; 46 } 47 return true; 48 } 49 50 // Not a DID - validate as handle 51 // Must have at least one dot 52 if !input.contains('.') { 53 return false; 54 } 55 56 // Must not have invalid characters 57 for c in input.chars() { 58 if !(c.is_ascii_lowercase() 59 || c.is_ascii_uppercase() 60 || c.is_ascii_digit() 61 || c == '.' 62 || c == '-') 63 { 64 return false; 65 } 66 } 67 68 // Basic length check 69 if input.len() > 253 { 70 return false; 71 } 72 73 // Must not start or end with dot or hyphen 74 if input.starts_with('.') 75 || input.ends_with('.') 76 || input.starts_with('-') 77 || input.ends_with('-') 78 { 79 return false; 80 } 81 82 true 83} 84 85/// Parse operation pointer: "bundle:position" or global position 86pub fn parse_operation_pointer(pointer: &str) -> anyhow::Result<(u32, usize)> { 87 // Check if it's "bundle:position" format 88 if let Some(colon_pos) = pointer.find(':') { 89 let bundle_str = &pointer[..colon_pos]; 90 let pos_str = &pointer[colon_pos + 1..]; 91 92 let bundle_num: u32 = bundle_str 93 .parse() 94 .map_err(|_| anyhow::anyhow!("Invalid bundle number: {}", bundle_str))?; 95 let position: usize = pos_str 96 .parse() 97 .map_err(|_| anyhow::anyhow!("Invalid position: {}", pos_str))?; 98 99 if bundle_num < 1 { 100 anyhow::bail!("Bundle number must be >= 1"); 101 } 102 103 return Ok((bundle_num, position)); 104 } 105 106 // Parse as global position 107 let global_pos: u64 = pointer.parse().map_err(|_| { 108 anyhow::anyhow!("Invalid position: must be number or 'bundle:position' format") 109 })?; 110 111 if global_pos < constants::BUNDLE_SIZE as u64 { 112 // Small numbers are shorthand for bundle 1 113 return Ok((1, global_pos as usize)); 114 } 115 116 // Convert global position to bundle + position 117 let (bundle_num, position) = crate::constants::global_to_bundle_position(global_pos); 118 119 Ok((bundle_num, position)) 120} 121 122/// Extract base URL from request headers and URI 123pub fn extract_base_url(headers: &HeaderMap, uri: &Uri) -> String { 124 if let Some(host_str) = headers.get("host").and_then(|h| h.to_str().ok()) { 125 // Check if request is HTTPS (from X-Forwarded-Proto or X-Forwarded-Ssl) 126 let is_https = headers 127 .get("x-forwarded-proto") 128 .and_then(|v| v.to_str().ok()) 129 .map(|s| s == "https") 130 .unwrap_or(false) 131 || headers 132 .get("x-forwarded-ssl") 133 .and_then(|v| v.to_str().ok()) 134 .map(|s| s == "on") 135 .unwrap_or(false); 136 137 let scheme = if is_https { "https" } else { "http" }; 138 return format!("{}://{}", scheme, host_str); 139 } 140 141 if let Some(authority) = uri.authority() { 142 format!("http://{}", authority) 143 } else { 144 "http://127.0.0.1:8080".to_string() 145 } 146} 147 148/// Create headers for bundle file download 149pub fn bundle_download_headers(content_type: &'static str, filename: &str) -> HeaderMap { 150 let mut headers = HeaderMap::new(); 151 headers.insert("Content-Type", HeaderValue::from_static(content_type)); 152 headers.insert( 153 "Content-Disposition", 154 HeaderValue::from_str(&format!("attachment; filename={}", filename)).unwrap(), 155 ); 156 headers 157} 158 159/// Parse duration string (e.g., "60s", "5m", "1h") into Duration 160pub fn parse_duration(s: &str) -> anyhow::Result<tokio::time::Duration> { 161 use anyhow::Context; 162 use tokio::time::Duration; 163 164 // Simple parser: "60s", "5m", "1h" 165 let s = s.trim(); 166 if let Some(stripped) = s.strip_suffix('s') { 167 let secs: u64 = stripped.parse().context("Invalid duration format")?; 168 Ok(Duration::from_secs(secs)) 169 } else if let Some(stripped) = s.strip_suffix('m') { 170 let mins: u64 = stripped.parse().context("Invalid duration format")?; 171 Ok(Duration::from_secs(mins * 60)) 172 } else if let Some(stripped) = s.strip_suffix('h') { 173 let hours: u64 = stripped.parse().context("Invalid duration format")?; 174 Ok(Duration::from_secs(hours * 3600)) 175 } else { 176 // Try parsing as seconds 177 let secs: u64 = s.parse().context("Invalid duration format")?; 178 Ok(Duration::from_secs(secs)) 179 } 180}