eure_env/cache/
path.rs

1//! Cache path computation.
2
3use sha2::{Digest, Sha256};
4use std::path::PathBuf;
5use url::Url;
6
7/// Information about a cache key derived from a URL.
8#[derive(Debug, Clone)]
9pub struct CacheKeyInfo {
10    /// Original URL
11    pub url: String,
12    /// SHA256 hash prefix (8 characters)
13    pub hash: String,
14    /// Host name from URL
15    pub host: String,
16    /// Original filename from URL path
17    pub filename: String,
18    /// Relative path within cache directory
19    pub cache_path: String,
20}
21
22/// Compute cache key information from a URL.
23///
24/// The cache path uses 2-level directory sharding to prevent directory overcrowding:
25/// `{host}/{hash[0:2]}/{hash[2:4]}/{hash}-{filename}`
26///
27/// Example:
28/// - URL: `https://eure.dev/v0.1.0/schemas/eure-schema.schema.eure`
29/// - Path: `eure.dev/a1/b2/a1b2c3d4-eure-schema.schema.eure`
30pub fn compute_cache_key(url: &Url) -> CacheKeyInfo {
31    let url_str = url.as_str();
32
33    // Compute SHA256 hash of the full URL
34    let mut hasher = Sha256::new();
35    hasher.update(url_str.as_bytes());
36    let hash_bytes = hasher.finalize();
37    let hash = hex::encode(&hash_bytes[..4]); // First 8 hex characters (4 bytes)
38
39    // Extract host
40    let host = url.host_str().unwrap_or("unknown").to_string();
41
42    // Extract filename from path
43    let path = url.path();
44    let filename = path
45        .rsplit('/')
46        .next()
47        .filter(|s| !s.is_empty())
48        .unwrap_or("index")
49        .to_string();
50
51    // Build cache path with 2-level sharding
52    let cache_path = format!(
53        "{}/{}/{}/{}-{}",
54        host,
55        &hash[0..2],
56        &hash[2..4],
57        hash,
58        filename
59    );
60
61    CacheKeyInfo {
62        url: url_str.to_string(),
63        hash,
64        host,
65        filename,
66        cache_path,
67    }
68}
69
70/// Convert URL to full cache file path.
71pub fn url_to_cache_path(url: &Url, cache_dir: &std::path::Path) -> PathBuf {
72    let key_info = compute_cache_key(url);
73    cache_dir.join(&key_info.cache_path)
74}
75
76/// Get the meta file path for a cache file.
77pub fn meta_path(cache_path: &std::path::Path) -> PathBuf {
78    let mut meta = cache_path.as_os_str().to_owned();
79    meta.push(".meta");
80    PathBuf::from(meta)
81}
82
83/// Get the lock file path for a cache file.
84pub fn lock_path(cache_path: &std::path::Path) -> PathBuf {
85    let mut lock = cache_path.as_os_str().to_owned();
86    lock.push(".lock");
87    PathBuf::from(lock)
88}
89
90/// Compute SHA256 hash of content and return as hex string.
91pub fn compute_content_hash(content: &str) -> String {
92    let mut hasher = Sha256::new();
93    hasher.update(content.as_bytes());
94    let hash_bytes = hasher.finalize();
95    hex::encode(&hash_bytes)
96}
97
98// Use hex crate for encoding
99mod hex {
100    const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
101
102    pub fn encode(bytes: &[u8]) -> String {
103        let mut result = String::with_capacity(bytes.len() * 2);
104        for &byte in bytes {
105            result.push(HEX_CHARS[(byte >> 4) as usize] as char);
106            result.push(HEX_CHARS[(byte & 0x0f) as usize] as char);
107        }
108        result
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_compute_cache_key() {
118        let url = Url::parse("https://eure.dev/v0.1.0/schemas/eure-schema.schema.eure").unwrap();
119        let key = compute_cache_key(&url);
120
121        assert_eq!(key.host, "eure.dev");
122        assert_eq!(key.filename, "eure-schema.schema.eure");
123        assert_eq!(key.hash.len(), 8);
124        assert!(key.cache_path.starts_with("eure.dev/"));
125        assert!(key.cache_path.contains(&key.hash));
126    }
127
128    #[test]
129    fn test_compute_cache_key_root_path() {
130        let url = Url::parse("https://example.com/").unwrap();
131        let key = compute_cache_key(&url);
132
133        assert_eq!(key.host, "example.com");
134        assert_eq!(key.filename, "index");
135    }
136
137    #[test]
138    fn test_meta_path() {
139        let cache_path = PathBuf::from("/cache/eure.dev/a1/b2/a1b2c3d4-schema.eure");
140        let meta = meta_path(&cache_path);
141        assert_eq!(
142            meta,
143            PathBuf::from("/cache/eure.dev/a1/b2/a1b2c3d4-schema.eure.meta")
144        );
145    }
146
147    #[test]
148    fn test_lock_path() {
149        let cache_path = PathBuf::from("/cache/eure.dev/a1/b2/a1b2c3d4-schema.eure");
150        let lock = lock_path(&cache_path);
151        assert_eq!(
152            lock,
153            PathBuf::from("/cache/eure.dev/a1/b2/a1b2c3d4-schema.eure.lock")
154        );
155    }
156
157    #[test]
158    fn test_compute_content_hash() {
159        // SHA256 of empty string
160        let hash = compute_content_hash("");
161        assert_eq!(
162            hash,
163            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
164        );
165
166        // SHA256 of "hello"
167        let hash = compute_content_hash("hello");
168        assert_eq!(
169            hash,
170            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
171        );
172    }
173
174    #[test]
175    fn test_url_to_cache_path() {
176        let url = Url::parse("https://eure.dev/schema.eure").unwrap();
177        let cache_dir = PathBuf::from("/home/user/.cache/eure/schemas");
178        let path = url_to_cache_path(&url, &cache_dir);
179
180        assert!(path.starts_with(&cache_dir));
181        assert!(path.to_string_lossy().contains("eure.dev"));
182    }
183}