1use alloc::string::String;
2
3pub const FONT_EXTENSIONS: &[&str] = &["ttf", "otf", "ttc", "woff", "woff2", "dfont"];
5
6pub const CONTENT_DEDUP_SAMPLE_BYTES: usize = 4096;
13
14pub fn content_dedup_hash_u64(bytes: &[u8]) -> u64 {
24 let len = bytes.len();
25 let head_len = len.min(CONTENT_DEDUP_SAMPLE_BYTES);
26 let tail_len = (len - head_len).min(CONTENT_DEDUP_SAMPLE_BYTES);
27 let tail_start = len - tail_len;
28 let mut seed_buf = [0u8; 8];
31 seed_buf.copy_from_slice(&(len as u64).to_le_bytes());
32 let seed = content_hash_u64(&seed_buf);
33 let head = content_hash_u64(&bytes[..head_len]);
34 let tail = content_hash_u64(&bytes[tail_start..tail_start + tail_len]);
35 const K: u64 = 0x9E3779B97F4A7C15;
38 let mut h = seed;
39 h ^= head;
40 h = h.wrapping_mul(K);
41 h ^= tail;
42 h = h.wrapping_mul(K);
43 h ^= h >> 33;
44 h
45}
46
47pub fn content_hash_u64(bytes: &[u8]) -> u64 {
60 const K: u64 = 0x9E3779B97F4A7C15;
63
64 let mut h: u64 = K ^ (bytes.len() as u64);
65 let chunks = bytes.chunks_exact(8);
66 let remainder = chunks.remainder();
67 for chunk in chunks {
68 let mut arr = [0u8; 8];
69 arr.copy_from_slice(chunk);
70 let v = u64::from_le_bytes(arr);
71 h = h.wrapping_add(v).wrapping_mul(K);
72 h ^= h >> 33;
73 }
74 let mut tail: u64 = 0;
76 for (i, b) in remainder.iter().enumerate() {
77 tail |= (*b as u64) << (i * 8);
78 }
79 h = h.wrapping_add(tail).wrapping_mul(K);
80 h ^= h >> 33;
81 h = h.wrapping_mul(K);
82 h ^= h >> 33;
83 h
84}
85
86pub fn normalize_family_name(name: &str) -> String {
90 name.chars()
91 .filter(|c| c.is_alphanumeric())
92 .map(|c| c.to_ascii_lowercase())
93 .collect()
94}
95
96#[cfg(feature = "std")]
98pub fn is_font_file(path: &std::path::Path) -> bool {
99 path.extension()
100 .and_then(|e| e.to_str())
101 .map(|ext| {
102 let lower = ext.to_lowercase();
103 FONT_EXTENSIONS.contains(&lower.as_str())
104 })
105 .unwrap_or(false)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn font_extensions_covers_common_formats() {
114 for ext in &["ttf", "otf", "ttc", "woff", "woff2"] {
115 assert!(FONT_EXTENSIONS.contains(ext), "missing extension: {}", ext);
116 }
117 }
118
119 #[cfg(feature = "std")]
120 #[test]
121 fn is_font_file_recognizes_fonts() {
122 use std::path::Path;
123 assert!(is_font_file(Path::new("Arial.ttf")));
124 assert!(is_font_file(Path::new("NotoSans.otf")));
125 assert!(is_font_file(Path::new("Font.TTC"))); assert!(is_font_file(Path::new("web.woff2")));
127 }
128
129 #[cfg(feature = "std")]
130 #[test]
131 fn is_font_file_rejects_non_fonts() {
132 use std::path::Path;
133 assert!(!is_font_file(Path::new("readme.txt")));
134 assert!(!is_font_file(Path::new("image.png")));
135 assert!(!is_font_file(Path::new("no_extension")));
136 }
137}