chie_shared/utils/
validation.rs

1//! Validation utility functions.
2
3use super::calculations::calculate_bandwidth_mbps;
4use super::formatting::sanitize_tag;
5use super::time::now_ms;
6
7/// Validate email format (basic check).
8///
9/// # Examples
10///
11/// ```
12/// use chie_shared::is_valid_email;
13///
14/// // Valid emails
15/// assert!(is_valid_email("user@example.com"));
16/// assert!(is_valid_email("test.user@domain.co.uk"));
17///
18/// // Invalid emails
19/// assert!(!is_valid_email("invalid"));
20/// assert!(!is_valid_email("@example.com"));
21/// assert!(!is_valid_email("user@"));
22/// assert!(!is_valid_email("user.example.com")); // Missing @
23/// ```
24#[inline]
25pub fn is_valid_email(email: &str) -> bool {
26    let parts: Vec<&str> = email.split('@').collect();
27    if parts.len() != 2 {
28        return false;
29    }
30
31    let local = parts[0];
32    let domain = parts[1];
33
34    !local.is_empty() && !domain.is_empty() && domain.contains('.')
35}
36
37/// Validate username (alphanumeric, underscore, hyphen, 3-20 chars).
38///
39/// # Examples
40///
41/// ```
42/// use chie_shared::is_valid_username;
43///
44/// // Valid usernames
45/// assert!(is_valid_username("alice"));
46/// assert!(is_valid_username("bob_123"));
47/// assert!(is_valid_username("user-name"));
48/// assert!(is_valid_username("test_user_2024"));
49///
50/// // Invalid usernames
51/// assert!(!is_valid_username("ab")); // Too short
52/// assert!(!is_valid_username("a".repeat(21).as_str())); // Too long
53/// assert!(!is_valid_username("user@name")); // Invalid character
54/// assert!(!is_valid_username("user name")); // Spaces not allowed
55/// ```
56#[inline]
57pub fn is_valid_username(username: &str) -> bool {
58    if username.len() < 3 || username.len() > 20 {
59        return false;
60    }
61
62    username
63        .chars()
64        .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
65}
66
67/// Parse content CID and validate basic format.
68///
69/// # Examples
70///
71/// ```
72/// use chie_shared::is_valid_cid;
73///
74/// // Valid CIDs (CIDv0 with Qm prefix)
75/// assert!(is_valid_cid("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"));
76///
77/// // Valid CIDs (CIDv1 with bafy prefix)
78/// assert!(is_valid_cid("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"));
79///
80/// // Invalid CIDs
81/// assert!(!is_valid_cid("invalid"));
82/// assert!(!is_valid_cid("Qm123")); // Too short
83/// assert!(!is_valid_cid("")); // Empty
84/// ```
85#[inline]
86pub fn is_valid_cid(cid: &str) -> bool {
87    // Basic CID validation: should start with 'Qm' or 'bafy' and be alphanumeric
88    if cid.is_empty() {
89        return false;
90    }
91
92    let valid_prefix = cid.starts_with("Qm") || cid.starts_with("bafy") || cid.starts_with("bafk");
93    let valid_chars = cid.chars().all(|c| c.is_alphanumeric());
94
95    valid_prefix && valid_chars && cid.len() >= 46
96}
97
98/// Check if a peer ID format is valid (basic check).
99///
100/// # Examples
101///
102/// ```
103/// use chie_shared::is_valid_peer_id;
104///
105/// // Valid libp2p peer IDs (start with 12D3Koo)
106/// assert!(is_valid_peer_id("12D3KooWD3bfmNbuuuM8puncXF4DxDWPTF8vK7X3K8K6z4Q7Q8Qp"));
107/// assert!(is_valid_peer_id("12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp"));
108///
109/// // Invalid peer IDs
110/// assert!(!is_valid_peer_id("invalid"));
111/// assert!(!is_valid_peer_id("Qm123")); // CID, not peer ID
112/// assert!(!is_valid_peer_id("12D3Koo")); // Too short
113/// ```
114#[inline]
115pub fn is_valid_peer_id(peer_id: &str) -> bool {
116    // libp2p peer IDs typically start with "12D3Koo" and are base58 encoded
117    peer_id.starts_with("12D3Koo") && peer_id.len() >= 46
118}
119
120/// Check if a string contains only safe characters (no control chars except whitespace).
121#[inline]
122pub fn is_safe_string(s: &str) -> bool {
123    s.chars().all(|c| !c.is_control() || c.is_whitespace())
124}
125
126/// Batch validate CIDs and return invalid ones.
127pub fn validate_cids_batch(cids: &[String]) -> Vec<String> {
128    cids.iter()
129        .filter(|cid| !is_valid_cid(cid))
130        .cloned()
131        .collect()
132}
133
134/// Batch validate emails and return invalid ones.
135pub fn validate_emails_batch(emails: &[String]) -> Vec<String> {
136    emails
137        .iter()
138        .filter(|email| !is_valid_email(email))
139        .cloned()
140        .collect()
141}
142
143/// Batch validate usernames and return invalid ones.
144pub fn validate_usernames_batch(usernames: &[String]) -> Vec<String> {
145    usernames
146        .iter()
147        .filter(|username| !is_valid_username(username))
148        .cloned()
149        .collect()
150}
151
152/// Validate IPv4 address format.
153///
154/// # Examples
155///
156/// ```
157/// use chie_shared::is_valid_ipv4;
158///
159/// // Valid IPv4 addresses
160/// assert!(is_valid_ipv4("192.168.1.1"));
161/// assert!(is_valid_ipv4("10.0.0.1"));
162/// assert!(is_valid_ipv4("127.0.0.1"));
163/// assert!(is_valid_ipv4("0.0.0.0"));
164/// assert!(is_valid_ipv4("255.255.255.255"));
165///
166/// // Invalid IPv4 addresses
167/// assert!(!is_valid_ipv4("256.1.1.1")); // Out of range
168/// assert!(!is_valid_ipv4("192.168.1")); // Missing octet
169/// assert!(!is_valid_ipv4("192.168.1.1.1")); // Too many octets
170/// assert!(!is_valid_ipv4("invalid"));
171/// ```
172pub fn is_valid_ipv4(ip: &str) -> bool {
173    let parts: Vec<&str> = ip.split('.').collect();
174    if parts.len() != 4 {
175        return false;
176    }
177
178    parts.iter().all(|part| part.parse::<u8>().is_ok())
179}
180
181/// Validate IPv6 address format (basic check).
182pub fn is_valid_ipv6(ip: &str) -> bool {
183    // Basic IPv6 validation: contains colons and valid hex characters
184    if !ip.contains(':') {
185        return false;
186    }
187
188    let parts: Vec<&str> = ip.split(':').collect();
189    if parts.len() < 3 || parts.len() > 8 {
190        return false;
191    }
192
193    parts
194        .iter()
195        .all(|part| part.is_empty() || part.chars().all(|c| c.is_ascii_hexdigit()))
196}
197
198/// Validate port number.
199pub fn is_valid_port(port: u16) -> bool {
200    port > 0
201}
202
203/// Parse multiaddr and check if it's valid (basic check).
204pub fn is_valid_multiaddr(addr: &str) -> bool {
205    // libp2p multiaddrs start with / and contain protocol identifiers
206    if !addr.starts_with('/') {
207        return false;
208    }
209
210    let parts: Vec<&str> = addr.split('/').filter(|s| !s.is_empty()).collect();
211    if parts.is_empty() {
212        return false;
213    }
214
215    // Check for common protocol identifiers
216    let valid_protocols = [
217        "ip4", "ip6", "tcp", "udp", "quic", "p2p", "ws", "wss", "http", "https",
218    ];
219    parts.iter().any(|part| valid_protocols.contains(part))
220}
221
222/// Validate HTTP/HTTPS URL format.
223///
224/// # Examples
225///
226/// ```
227/// use chie_shared::is_valid_url;
228///
229/// // Valid URLs
230/// assert!(is_valid_url("https://example.com"));
231/// assert!(is_valid_url("http://localhost:8080/api"));
232/// assert!(is_valid_url("https://api.example.com/v1/users?id=123"));
233///
234/// // Invalid URLs
235/// assert!(!is_valid_url("ftp://example.com")); // Wrong protocol
236/// assert!(!is_valid_url("example.com")); // Missing protocol
237/// assert!(!is_valid_url("")); // Empty
238/// ```
239pub fn is_valid_url(url: &str) -> bool {
240    if url.is_empty() || url.len() > 2048 {
241        return false;
242    }
243
244    url.starts_with("http://") || url.starts_with("https://")
245}
246
247/// Verify hex string is valid.
248pub fn is_valid_hex(hex: &str) -> bool {
249    hex.len() % 2 == 0 && hex.chars().all(|c| c.is_ascii_hexdigit())
250}
251
252/// Validate chunk size is within acceptable range.
253pub fn validate_chunk_size(size: usize, min: usize, max: usize) -> bool {
254    size >= min && size <= max
255}
256
257/// Validate proof timestamp is recent (within tolerance).
258pub fn validate_proof_freshness(timestamp_ms: i64, tolerance_ms: i64) -> bool {
259    let now = now_ms();
260    let age = now - timestamp_ms;
261    age >= 0 && age <= tolerance_ms
262}
263
264/// Validate latency is within acceptable range.
265pub fn validate_latency(latency_ms: u32, max_latency_ms: u32) -> bool {
266    latency_ms <= max_latency_ms
267}
268
269/// Validate bandwidth is not suspiciously high.
270pub fn validate_bandwidth_reasonable(bytes: u64, duration_ms: u64, max_mbps: f64) -> bool {
271    if duration_ms == 0 {
272        return false;
273    }
274
275    let mbps = calculate_bandwidth_mbps(bytes, duration_ms);
276    mbps <= max_mbps
277}
278
279/// Validate nonce has correct length.
280pub fn validate_nonce_length(nonce: &[u8], expected_len: usize) -> bool {
281    nonce.len() == expected_len
282}
283
284/// Validate signature has correct length.
285pub fn validate_signature_length(signature: &[u8], expected_len: usize) -> bool {
286    signature.len() == expected_len
287}
288
289/// Validate public key has correct length.
290pub fn validate_public_key_length(public_key: &[u8], expected_len: usize) -> bool {
291    public_key.len() == expected_len
292}
293
294/// Validate hash has correct length.
295pub fn validate_hash_length(hash: &[u8], expected_len: usize) -> bool {
296    hash.len() == expected_len
297}
298
299/// Batch validate chunk indices are within bounds.
300pub fn validate_chunk_indices_batch(indices: &[u64], max_index: u64) -> Vec<u64> {
301    indices
302        .iter()
303        .filter(|&&idx| idx >= max_index)
304        .copied()
305        .collect()
306}
307
308/// Validate content size is within platform limits.
309pub fn validate_content_size_in_range(size: u64, min: u64, max: u64) -> bool {
310    size >= min && size <= max
311}
312
313/// Validate price is within acceptable range.
314pub fn validate_price_range(price: u64, min: u64, max: u64) -> bool {
315    price >= min && price <= max
316}
317
318/// Sanitize and validate tag.
319pub fn validate_and_sanitize_tag(tag: &str, max_len: usize) -> Option<String> {
320    let sanitized = sanitize_tag(tag);
321    if sanitized.is_empty() || sanitized.len() > max_len {
322        None
323    } else {
324        Some(sanitized)
325    }
326}
327
328/// Validate all tags in a list and return valid ones.
329pub fn validate_tags_list(tags: &[String], max_len: usize, max_count: usize) -> Vec<String> {
330    tags.iter()
331        .take(max_count)
332        .filter_map(|t| validate_and_sanitize_tag(t, max_len))
333        .collect()
334}
335
336/// Validate Ed25519 signature format (64 bytes).
337pub fn validate_ed25519_signature(signature: &[u8]) -> bool {
338    validate_signature_length(signature, 64)
339}
340
341/// Validate Ed25519 public key format (32 bytes).
342pub fn validate_ed25519_public_key(public_key: &[u8]) -> bool {
343    validate_public_key_length(public_key, 32)
344}
345
346/// Validate BLAKE3 hash format (32 bytes).
347pub fn validate_blake3_hash(hash: &[u8]) -> bool {
348    validate_hash_length(hash, 32)
349}
350
351/// Validate challenge nonce format (32 bytes).
352pub fn validate_challenge_nonce(nonce: &[u8]) -> bool {
353    validate_nonce_length(nonce, 32)
354}
355
356/// Check if an IPv4 address is in a private range.
357/// Private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8.
358pub fn is_private_ipv4(ip: &str) -> bool {
359    let parts: Vec<&str> = ip.split('.').collect();
360    if parts.len() != 4 {
361        return false;
362    }
363
364    let octets: Result<Vec<u8>, _> = parts.iter().map(|s| s.parse()).collect();
365    if let Ok(octets) = octets {
366        if octets.len() != 4 {
367            return false;
368        }
369
370        // 10.0.0.0/8
371        if octets[0] == 10 {
372            return true;
373        }
374
375        // 172.16.0.0/12
376        if octets[0] == 172 && (16..=31).contains(&octets[1]) {
377            return true;
378        }
379
380        // 192.168.0.0/16
381        if octets[0] == 192 && octets[1] == 168 {
382            return true;
383        }
384
385        // 127.0.0.0/8 (loopback)
386        if octets[0] == 127 {
387            return true;
388        }
389
390        false
391    } else {
392        false
393    }
394}
395
396/// Validate URL-safe string (alphanumeric, dash, underscore)
397#[must_use]
398#[allow(dead_code)]
399pub fn validate_url_safe_string(s: &str) -> bool {
400    !s.is_empty()
401        && s.chars()
402            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
403}
404
405/// Validate that a string is valid JSON
406#[must_use]
407#[allow(dead_code)]
408pub fn validate_json_string(s: &str) -> bool {
409    serde_json::from_str::<serde_json::Value>(s).is_ok()
410}
411
412/// Validate semantic version format (e.g., "1.2.3", "1.0.0-beta")
413#[must_use]
414#[allow(dead_code)]
415pub fn validate_semver(version: &str) -> bool {
416    // Split on dash first to handle prerelease
417    let main_prerelease: Vec<&str> = version.splitn(2, '-').collect();
418    let main_version = main_prerelease[0];
419
420    // Validate major.minor.patch
421    let parts: Vec<&str> = main_version.split('.').collect();
422    if parts.len() != 3 {
423        return false;
424    }
425
426    // All parts must be numbers
427    if !parts
428        .iter()
429        .all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit()))
430    {
431        return false;
432    }
433
434    // If there's a prerelease part, validate it
435    if main_prerelease.len() > 1 {
436        let prerelease = main_prerelease[1];
437        !prerelease.is_empty()
438            && prerelease
439                .chars()
440                .all(|c| c.is_alphanumeric() || c == '.' || c == '-')
441    } else {
442        true
443    }
444}
445
446/// Validate UUID v4 format
447#[must_use]
448#[allow(dead_code)]
449pub fn validate_uuid_v4(uuid: &str) -> bool {
450    if uuid.len() != 36 {
451        return false;
452    }
453
454    let parts: Vec<&str> = uuid.split('-').collect();
455    if parts.len() != 5 {
456        return false;
457    }
458
459    // Check lengths: 8-4-4-4-12
460    if parts[0].len() != 8
461        || parts[1].len() != 4
462        || parts[2].len() != 4
463        || parts[3].len() != 4
464        || parts[4].len() != 12
465    {
466        return false;
467    }
468
469    // Verify version 4 (3rd section should start with 4)
470    if !parts[2].starts_with('4') {
471        return false;
472    }
473
474    // All parts should be valid hex
475    parts
476        .iter()
477        .all(|part| part.chars().all(|c| c.is_ascii_hexdigit()))
478}
479
480/// Validate hex color code (#RGB or #RRGGBB)
481#[must_use]
482#[allow(dead_code)]
483pub fn validate_hex_color(color: &str) -> bool {
484    if !color.starts_with('#') {
485        return false;
486    }
487
488    let hex = &color[1..];
489    (hex.len() == 3 || hex.len() == 6) && hex.chars().all(|c| c.is_ascii_hexdigit())
490}
491
492/// Validate port number is in valid range (1-65535)
493#[must_use]
494#[allow(dead_code)]
495pub fn validate_port_range(port: u16) -> bool {
496    port > 0
497}
498
499/// Validate Content-Type/MIME type format
500#[must_use]
501#[allow(dead_code)]
502pub fn validate_content_type(content_type: &str) -> bool {
503    let parts: Vec<&str> = content_type.split('/').collect();
504    if parts.len() != 2 {
505        return false;
506    }
507
508    let type_valid = !parts[0].is_empty()
509        && parts[0]
510            .chars()
511            .all(|c| c.is_alphanumeric() || c == '-' || c == '+');
512
513    let subtype = parts[1].split(';').next().unwrap_or("");
514    let subtype_valid = !subtype.is_empty()
515        && subtype
516            .chars()
517            .all(|c| c.is_alphanumeric() || c == '-' || c == '+' || c == '.');
518
519    type_valid && subtype_valid
520}
521
522/// Validate that a string contains only printable ASCII characters
523#[must_use]
524#[allow(dead_code)]
525pub fn validate_printable_ascii(s: &str) -> bool {
526    s.chars().all(|c| c.is_ascii() && !c.is_ascii_control())
527}
528
529/// Validate base64 string format
530#[must_use]
531#[allow(dead_code)]
532pub fn validate_base64(s: &str) -> bool {
533    if s.is_empty() || s.len() % 4 != 0 {
534        return false;
535    }
536
537    let bytes = s.as_bytes();
538    let len = bytes.len();
539
540    for (i, &byte) in bytes.iter().enumerate() {
541        let c = byte as char;
542        if c == '=' {
543            // '=' can only be at the last or second-to-last position
544            if i != len - 1 && i != len - 2 {
545                return false;
546            }
547            // If second-to-last is '=', last must also be '='
548            if i == len - 2 && bytes[len - 1] != b'=' {
549                return false;
550            }
551        } else if !c.is_alphanumeric() && c != '+' && c != '/' {
552            return false;
553        }
554    }
555
556    true
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn test_is_valid_email() {
565        assert!(is_valid_email("test@example.com"));
566        assert!(is_valid_email("user+tag@domain.co.jp"));
567        assert!(!is_valid_email("invalid"));
568        assert!(!is_valid_email("@example.com"));
569        assert!(!is_valid_email("test@"));
570        assert!(!is_valid_email("test@com"));
571    }
572
573    #[test]
574    fn test_is_valid_username() {
575        assert!(is_valid_username("alice"));
576        assert!(is_valid_username("alice_123"));
577        assert!(is_valid_username("alice-bob"));
578        assert!(!is_valid_username("ab"));
579        assert!(!is_valid_username(&"a".repeat(21)));
580        assert!(!is_valid_username("alice bob"));
581        assert!(!is_valid_username("alice@bob"));
582    }
583
584    #[test]
585    fn test_is_valid_cid() {
586        assert!(is_valid_cid(
587            "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"
588        ));
589        assert!(is_valid_cid(
590            "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
591        ));
592        assert!(!is_valid_cid("invalid"));
593        assert!(!is_valid_cid(""));
594        assert!(!is_valid_cid("Qm"));
595    }
596
597    #[test]
598    fn test_is_valid_peer_id() {
599        assert!(is_valid_peer_id(
600            "12D3KooWRQTwRLgXZfYJnAL8fCF7VBvPgPDJxz3cBdJq9BvZT8bT"
601        ));
602        assert!(!is_valid_peer_id("invalid"));
603        assert!(!is_valid_peer_id("12D3Koo"));
604    }
605
606    #[test]
607    fn test_is_safe_string() {
608        assert!(is_safe_string("hello world"));
609        assert!(is_safe_string("hello\nworld"));
610        assert!(is_safe_string("hello\tworld"));
611        assert!(!is_safe_string("hello\x00world"));
612        assert!(!is_safe_string("hello\x01world"));
613    }
614
615    #[test]
616    fn test_validate_cids_batch() {
617        let cids = vec![
618            "QmValidCID1234567890123456789012345678901234567890".to_string(),
619            "invalid".to_string(),
620            "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string(),
621            "".to_string(),
622        ];
623        let invalid = validate_cids_batch(&cids);
624        assert_eq!(invalid.len(), 2);
625        assert!(invalid.contains(&"invalid".to_string()));
626        assert!(invalid.contains(&"".to_string()));
627    }
628
629    #[test]
630    fn test_validate_emails_batch() {
631        let emails = vec![
632            "valid@example.com".to_string(),
633            "invalid".to_string(),
634            "another@test.org".to_string(),
635            "@invalid.com".to_string(),
636        ];
637        let invalid = validate_emails_batch(&emails);
638        assert_eq!(invalid.len(), 2);
639    }
640
641    #[test]
642    fn test_validate_usernames_batch() {
643        let usernames = vec![
644            "validuser123".to_string(),
645            "ab".to_string(), // too short
646            "user_name".to_string(),
647            "a".repeat(25), // too long
648        ];
649        let invalid = validate_usernames_batch(&usernames);
650        assert_eq!(invalid.len(), 2);
651    }
652
653    #[test]
654    fn test_is_valid_ipv4() {
655        assert!(is_valid_ipv4("192.168.1.1"));
656        assert!(is_valid_ipv4("127.0.0.1"));
657        assert!(is_valid_ipv4("255.255.255.255"));
658        assert!(!is_valid_ipv4("256.1.1.1"));
659        assert!(!is_valid_ipv4("192.168.1"));
660        assert!(!is_valid_ipv4("192.168.1.1.1"));
661        assert!(!is_valid_ipv4("not.an.ip.addr"));
662    }
663
664    #[test]
665    fn test_is_valid_ipv6() {
666        assert!(is_valid_ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
667        assert!(is_valid_ipv6("2001:db8::1"));
668        assert!(is_valid_ipv6("::1"));
669        assert!(!is_valid_ipv6("192.168.1.1"));
670        assert!(!is_valid_ipv6("not:valid:ipv6:xyz"));
671        assert!(!is_valid_ipv6(""));
672    }
673
674    #[test]
675    fn test_is_valid_port() {
676        assert!(is_valid_port(80));
677        assert!(is_valid_port(443));
678        assert!(is_valid_port(65535));
679        assert!(!is_valid_port(0));
680    }
681
682    #[test]
683    fn test_is_valid_multiaddr() {
684        assert!(is_valid_multiaddr("/ip4/127.0.0.1/tcp/4001"));
685        assert!(is_valid_multiaddr("/ip6/::1/tcp/4001"));
686        assert!(is_valid_multiaddr("/ip4/192.168.1.1/tcp/8080/ws"));
687        assert!(!is_valid_multiaddr("invalid"));
688        assert!(!is_valid_multiaddr(""));
689        assert!(!is_valid_multiaddr("/invalid/address"));
690    }
691
692    #[test]
693    fn test_is_valid_url() {
694        assert!(is_valid_url("https://example.com"));
695        assert!(is_valid_url("http://example.com"));
696        assert!(is_valid_url("https://example.com/path?query=value"));
697        assert!(!is_valid_url("ftp://example.com"));
698        assert!(!is_valid_url(""));
699        assert!(!is_valid_url("not a url"));
700        // Test URL that's too long (> 2048 chars)
701        let long_url = format!("https://{}", "a".repeat(3000));
702        assert!(!is_valid_url(&long_url));
703    }
704
705    #[test]
706    fn test_is_valid_hex() {
707        assert!(is_valid_hex("deadbeef"));
708        assert!(is_valid_hex("0123456789abcdef"));
709        assert!(is_valid_hex(""));
710        assert!(!is_valid_hex("abc")); // Odd length
711        assert!(!is_valid_hex("xyz")); // Invalid characters
712    }
713
714    #[test]
715    fn test_validate_chunk_size() {
716        assert!(validate_chunk_size(1024, 512, 2048));
717        assert!(!validate_chunk_size(256, 512, 2048));
718        assert!(!validate_chunk_size(4096, 512, 2048));
719    }
720
721    #[test]
722    fn test_validate_proof_freshness() {
723        let now = now_ms();
724        assert!(validate_proof_freshness(now, 5000));
725        assert!(validate_proof_freshness(now - 1000, 5000));
726        assert!(!validate_proof_freshness(now - 10000, 5000));
727        assert!(!validate_proof_freshness(now + 1000, 5000));
728    }
729
730    #[test]
731    fn test_validate_latency() {
732        assert!(validate_latency(100, 500));
733        assert!(validate_latency(500, 500));
734        assert!(!validate_latency(501, 500));
735    }
736
737    #[test]
738    fn test_validate_bandwidth_reasonable() {
739        // 100 MB in 10 seconds = 80 Mbps (reasonable)
740        assert!(validate_bandwidth_reasonable(100_000_000, 10_000, 1000.0));
741
742        // 10 GB in 1 second = 80 Gbps (unreasonable for 1000 Mbps limit)
743        assert!(!validate_bandwidth_reasonable(
744            10_000_000_000,
745            1_000,
746            1000.0
747        ));
748
749        // Zero duration should fail
750        assert!(!validate_bandwidth_reasonable(1000, 0, 1000.0));
751    }
752
753    #[test]
754    fn test_validate_nonce_length() {
755        assert!(validate_nonce_length(&[0u8; 32], 32));
756        assert!(!validate_nonce_length(&[0u8; 16], 32));
757        assert!(!validate_nonce_length(&[0u8; 64], 32));
758    }
759
760    #[test]
761    fn test_validate_signature_length() {
762        assert!(validate_signature_length(&[0u8; 64], 64));
763        assert!(!validate_signature_length(&[0u8; 32], 64));
764    }
765
766    #[test]
767    fn test_validate_public_key_length() {
768        assert!(validate_public_key_length(&[0u8; 32], 32));
769        assert!(!validate_public_key_length(&[0u8; 64], 32));
770    }
771
772    #[test]
773    fn test_validate_hash_length() {
774        assert!(validate_hash_length(&[0u8; 32], 32));
775        assert!(!validate_hash_length(&[0u8; 16], 32));
776    }
777
778    #[test]
779    fn test_validate_chunk_indices_batch() {
780        let indices = vec![0, 1, 5, 10, 15];
781        let invalid = validate_chunk_indices_batch(&indices, 10);
782        assert_eq!(invalid, vec![10, 15]);
783
784        let all_valid = validate_chunk_indices_batch(&indices, 20);
785        assert!(all_valid.is_empty());
786    }
787
788    #[test]
789    fn test_validate_content_size_in_range() {
790        assert!(validate_content_size_in_range(1024, 512, 2048));
791        assert!(validate_content_size_in_range(512, 512, 2048));
792        assert!(validate_content_size_in_range(2048, 512, 2048));
793        assert!(!validate_content_size_in_range(256, 512, 2048));
794        assert!(!validate_content_size_in_range(4096, 512, 2048));
795    }
796
797    #[test]
798    fn test_validate_price_range() {
799        assert!(validate_price_range(100, 1, 1000));
800        assert!(!validate_price_range(0, 1, 1000));
801        assert!(!validate_price_range(1001, 1, 1000));
802    }
803
804    #[test]
805    fn test_validate_and_sanitize_tag() {
806        assert_eq!(
807            validate_and_sanitize_tag("  Rust  ", 20),
808            Some("rust".to_string())
809        );
810        assert_eq!(validate_and_sanitize_tag("  ", 20), None);
811        assert_eq!(validate_and_sanitize_tag("verylongtagname", 10), None);
812    }
813
814    #[test]
815    fn test_validate_tags_list() {
816        let tags = vec![
817            "Rust".to_string(),
818            "  Python  ".to_string(),
819            "  ".to_string(),
820            "JavaScript".to_string(),
821            "Go".to_string(),
822        ];
823
824        // Empty tag gets filtered out, so we get 4 valid tags
825        // but with max_count=3, we take first 3 and filter
826        let valid = validate_tags_list(&tags, 20, 3);
827        // First 3 are Rust, Python, empty (filtered), so we get rust, python
828        // Then we check next item which is JavaScript
829        // Actually, the function takes first 3 items then filters, so:
830        // Rust -> rust, Python -> python, empty -> None, so we get ["rust", "python"]
831        assert_eq!(valid, vec!["rust", "python"]);
832
833        let limited = validate_tags_list(&tags, 20, 2);
834        assert_eq!(limited.len(), 2);
835    }
836
837    #[test]
838    fn test_validate_ed25519_signature() {
839        assert!(validate_ed25519_signature(&[0u8; 64]));
840        assert!(!validate_ed25519_signature(&[0u8; 32]));
841    }
842
843    #[test]
844    fn test_validate_ed25519_public_key() {
845        assert!(validate_ed25519_public_key(&[0u8; 32]));
846        assert!(!validate_ed25519_public_key(&[0u8; 64]));
847    }
848
849    #[test]
850    fn test_validate_blake3_hash() {
851        assert!(validate_blake3_hash(&[0u8; 32]));
852        assert!(!validate_blake3_hash(&[0u8; 16]));
853    }
854
855    #[test]
856    fn test_validate_challenge_nonce() {
857        assert!(validate_challenge_nonce(&[0u8; 32]));
858        assert!(!validate_challenge_nonce(&[0u8; 16]));
859    }
860
861    #[test]
862    fn test_is_private_ipv4() {
863        assert!(is_private_ipv4("10.0.0.1"));
864        assert!(is_private_ipv4("10.255.255.255"));
865        assert!(is_private_ipv4("172.16.0.1"));
866        assert!(is_private_ipv4("172.31.255.255"));
867        assert!(is_private_ipv4("192.168.1.1"));
868        assert!(is_private_ipv4("192.168.255.255"));
869        assert!(is_private_ipv4("127.0.0.1"));
870        assert!(!is_private_ipv4("8.8.8.8"));
871        assert!(!is_private_ipv4("1.2.3.4"));
872        assert!(!is_private_ipv4("172.15.0.1")); // Not in 172.16.0.0/12
873        assert!(!is_private_ipv4("172.32.0.1")); // Not in 172.16.0.0/12
874        assert!(!is_private_ipv4("invalid"));
875    }
876
877    // New validation helper tests
878    #[test]
879    fn test_validate_url_safe_string() {
880        assert!(validate_url_safe_string("hello-world_123"));
881        assert!(validate_url_safe_string("test_slug"));
882        assert!(validate_url_safe_string("abc-123"));
883        assert!(!validate_url_safe_string(""));
884        assert!(!validate_url_safe_string("hello world")); // Space
885        assert!(!validate_url_safe_string("test@example")); // Special char
886    }
887
888    #[test]
889    fn test_validate_json_string() {
890        assert!(validate_json_string(r#"{"key": "value"}"#));
891        assert!(validate_json_string(r"[1, 2, 3]"));
892        assert!(validate_json_string(r"null"));
893        assert!(validate_json_string(r"true"));
894        assert!(validate_json_string(r"42"));
895        assert!(!validate_json_string(r"{invalid}"));
896        assert!(!validate_json_string(r"not json"));
897    }
898
899    #[test]
900    fn test_validate_semver() {
901        assert!(validate_semver("1.0.0"));
902        assert!(validate_semver("1.2.3"));
903        assert!(validate_semver("0.0.1"));
904        assert!(validate_semver("1.0.0-beta"));
905        assert!(validate_semver("2.1.3-alpha.1"));
906        assert!(!validate_semver("1.0")); // Missing patch
907        assert!(!validate_semver("1.0.0.0")); // Extra version
908        assert!(!validate_semver("a.b.c")); // Not numbers
909        assert!(!validate_semver("1.2.")); // Empty patch
910    }
911
912    #[test]
913    fn test_validate_uuid_v4() {
914        assert!(validate_uuid_v4("550e8400-e29b-41d4-a716-446655440000"));
915        assert!(validate_uuid_v4("f47ac10b-58cc-4372-a567-0e02b2c3d479"));
916        assert!(!validate_uuid_v4("550e8400-e29b-31d4-a716-446655440000")); // Not v4
917        assert!(!validate_uuid_v4("550e8400-e29b-41d4-a716")); // Too short
918        assert!(!validate_uuid_v4("not-a-uuid"));
919        assert!(!validate_uuid_v4("550e8400e29b41d4a716446655440000")); // No dashes
920    }
921
922    #[test]
923    fn test_validate_hex_color() {
924        assert!(validate_hex_color("#FFF"));
925        assert!(validate_hex_color("#fff"));
926        assert!(validate_hex_color("#FFFFFF"));
927        assert!(validate_hex_color("#123abc"));
928        assert!(!validate_hex_color("FFF")); // No #
929        assert!(!validate_hex_color("#FF")); // Too short
930        assert!(!validate_hex_color("#FFFFFFF")); // Too long
931        assert!(!validate_hex_color("#GGG")); // Invalid hex
932    }
933
934    #[test]
935    fn test_validate_port_range() {
936        assert!(validate_port_range(1));
937        assert!(validate_port_range(80));
938        assert!(validate_port_range(8080));
939        assert!(validate_port_range(65535));
940        assert!(!validate_port_range(0));
941    }
942
943    #[test]
944    fn test_validate_content_type() {
945        assert!(validate_content_type("text/plain"));
946        assert!(validate_content_type("application/json"));
947        assert!(validate_content_type("image/png"));
948        assert!(validate_content_type("text/html; charset=utf-8"));
949        assert!(validate_content_type("application/vnd.api+json"));
950        assert!(!validate_content_type("invalid"));
951        assert!(!validate_content_type("/json")); // No type
952        assert!(!validate_content_type("text/")); // No subtype
953    }
954
955    #[test]
956    fn test_validate_printable_ascii() {
957        assert!(validate_printable_ascii("Hello World 123"));
958        assert!(validate_printable_ascii("test@example.com"));
959        assert!(!validate_printable_ascii("hello\nworld")); // Newline
960        assert!(!validate_printable_ascii("test\0null")); // Null byte
961        assert!(!validate_printable_ascii("hello\tworld")); // Tab
962    }
963
964    #[test]
965    fn test_validate_base64() {
966        assert!(validate_base64("SGVsbG8=")); // "Hello" in base64
967        assert!(validate_base64("YWJjMTIz")); // "abc123" in base64
968        assert!(validate_base64("dGVzdA==")); // "test" in base64
969        assert!(!validate_base64("invalid!")); // Invalid char
970        assert!(!validate_base64("abc")); // Wrong length (not multiple of 4)
971        assert!(!validate_base64("ab=c")); // = in wrong position
972    }
973}