use url::Url;
pub fn is_valid_datetime(s: &str) -> bool {
if s.len() < 19 {
return false;
}
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().take(19).enumerate() {
let ok = match i {
4 | 7 => b == b'-',
10 => b == b'T',
13 | 16 => b == b':',
_ => b.is_ascii_digit(),
};
if !ok {
return false;
}
}
true
}
pub fn is_valid_duration(s: &str) -> bool {
if s.is_empty() || !s.starts_with('P') {
return false;
}
let rest = &s[1..];
if rest.is_empty() {
return false;
}
let valid_designators = b"YMWDHSMT";
let mut saw_any = false;
for &b in rest.as_bytes() {
if b.is_ascii_digit() {
saw_any = true;
continue;
}
if !valid_designators.contains(&b) {
return false;
}
}
saw_any
}
pub fn is_valid_timezone(s: &str) -> bool {
if s.is_empty() || !s.contains('/') {
return false;
}
s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '_' | '-' | '+'))
}
pub fn is_valid_hex_color(s: &str) -> bool {
if !s.starts_with('#') {
return false;
}
let hex = &s[1..];
(hex.len() == 3 || hex.len() == 6) && hex.chars().all(|c| c.is_ascii_hexdigit())
}
pub fn is_valid_rrule(s: &str) -> bool {
if s.is_empty() {
return false;
}
let parts: Vec<&str> = s.split(';').collect();
if parts.is_empty() {
return false;
}
let mut has_freq = false;
for part in parts {
let mut kv = part.splitn(2, '=');
let key = match kv.next() {
Some(k) => k,
None => return false,
};
if kv.next().is_none() {
return false;
}
if key == "FREQ" {
has_freq = true;
}
}
has_freq
}
pub fn validate_pubky_uri(uri: &str) -> Result<(), String> {
let parsed =
Url::parse(uri).map_err(|_| format!("Validation Error: Invalid URI format: {}", uri))?;
if parsed.scheme() != "pubky" {
return Err(format!(
"Validation Error: URI must use pubky:// protocol: {}",
uri
));
}
Ok(())
}
pub fn validate_timestamp_microseconds(ts_us: i64, field: &str) -> Result<(), String> {
if ts_us <= 0 {
return Err(format!(
"Validation Error: {field} must be a positive UNIX timestamp in microseconds, got {ts_us}"
));
}
let now_us = crate::common::timestamp();
let max_future_us = now_us + 86_400_000_000; if ts_us > max_future_us {
return Err(format!(
"Validation Error: {field} {ts_us} is more than 1 day in the future"
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_datetime_valid() {
assert!(is_valid_datetime("2025-12-01T10:00:00"));
assert!(is_valid_datetime("2025-12-01T10:00:00Z"));
assert!(is_valid_datetime("2025-12-01T10:00:00+02:00"));
}
#[test]
fn test_datetime_invalid() {
assert!(!is_valid_datetime("2025-12-01"));
assert!(!is_valid_datetime("not a date"));
assert!(!is_valid_datetime(""));
}
#[test]
fn test_duration_valid() {
assert!(is_valid_duration("PT1H"));
assert!(is_valid_duration("PT30M"));
assert!(is_valid_duration("P1D"));
assert!(is_valid_duration("P1DT2H30M"));
}
#[test]
fn test_duration_invalid() {
assert!(!is_valid_duration("1H"));
assert!(!is_valid_duration(""));
assert!(!is_valid_duration("P"));
}
#[test]
fn test_timezone_valid() {
assert!(is_valid_timezone("Europe/Zurich"));
assert!(is_valid_timezone("America/New_York"));
assert!(is_valid_timezone("Asia/Tokyo"));
}
#[test]
fn test_timezone_invalid() {
assert!(!is_valid_timezone(""));
assert!(!is_valid_timezone("Invalid"));
assert!(!is_valid_timezone("Europe@Zurich"));
}
#[test]
fn test_hex_color() {
assert!(is_valid_hex_color("#fff"));
assert!(is_valid_hex_color("#FFFFFF"));
assert!(is_valid_hex_color("#123456"));
assert!(!is_valid_hex_color("fff"));
assert!(!is_valid_hex_color("#xyz"));
}
#[test]
fn test_rrule() {
assert!(is_valid_rrule("FREQ=DAILY"));
assert!(is_valid_rrule("FREQ=WEEKLY;COUNT=10"));
assert!(!is_valid_rrule("COUNT=10"));
assert!(!is_valid_rrule(""));
}
}