repro_env/
paths.rs

1use crate::errors::*;
2use std::env;
3use std::io::ErrorKind;
4use std::path::{Component, Path, PathBuf};
5use tokio::fs;
6
7static SHARD_SIZE: usize = 2;
8
9pub fn repro_env_dir() -> Result<PathBuf> {
10    if let Some(path) = env::var_os("REPRO_ENV_HOME") {
11        Ok(path.into())
12    } else {
13        let mut cache = dirs::cache_dir().context("Failed to detect cache directory")?;
14        cache.push("repro-env");
15        Ok(cache)
16    }
17}
18
19pub fn cache_dir() -> Result<PathBuf> {
20    if let Some(path) = env::var_os("REPRO_ENV_CACHE") {
21        Ok(path.into())
22    } else {
23        repro_env_dir()
24    }
25}
26
27pub fn pkgs_cache_dir() -> Result<PkgsCacheDir> {
28    let mut path = cache_dir()?;
29    path.push("pkgs");
30    Ok(PkgsCacheDir { path })
31}
32
33pub fn alpine_cache_dir() -> Result<PkgsCacheDir> {
34    let mut path = cache_dir()?;
35    path.push("alpine");
36    Ok(PkgsCacheDir { path })
37}
38
39#[derive(Debug)]
40pub struct PkgsCacheDir {
41    path: PathBuf,
42}
43
44impl PkgsCacheDir {
45    fn shard<'a>(hash: &'a str, algo: &'static str, len: usize) -> Result<(&'a str, &'a str)> {
46        if hash.len() != len {
47            bail!("Unexpected {algo} checksum length: {:?}", hash.len());
48        }
49        if !hash.chars().all(char::is_alphanumeric) {
50            bail!("Unexpected characters in {algo}: {hash:?}");
51        }
52
53        let shard = &hash[..SHARD_SIZE];
54        let suffix = &hash[SHARD_SIZE..];
55        Ok((shard, suffix))
56    }
57
58    fn shard_sha256(sha256: &str) -> Result<(&str, &str)> {
59        Self::shard(sha256, "sha256", 64)
60    }
61
62    fn shard_sha1(sha1: &str) -> Result<(&str, &str)> {
63        Self::shard(sha1, "sha1", 40)
64    }
65
66    pub fn sha256_path(&self, sha256: &str) -> Result<PathBuf> {
67        let (shard, suffix) = Self::shard_sha256(sha256)?;
68
69        let mut path = self.path.clone();
70        path.push(shard);
71        path.push(suffix);
72
73        Ok(path)
74    }
75
76    fn sha1_path(&self, sha1: &str) -> Result<PathBuf> {
77        let (shard, suffix) = Self::shard_sha1(sha1)?;
78
79        let mut path = self.path.clone();
80        path.push(shard);
81        path.push(suffix);
82
83        Ok(path)
84    }
85
86    pub async fn sha1_read_link(&self, sha1: &str) -> Result<Option<String>> {
87        let path = self.sha1_path(sha1)?;
88        match fs::read_link(&path).await {
89            Ok(path) => {
90                trace!("Found symlink in cache: {path:?}");
91                let sha256 = Self::link_to_sha256(&path)?;
92                Ok(Some(sha256))
93            }
94            Err(err) if err.kind() == ErrorKind::NotFound => {
95                trace!("Did not find symlink in cache: {path:?}");
96                Ok(None)
97            }
98            Err(err) => Err(err.into()),
99        }
100    }
101
102    pub fn sha1_to_sha256(&self, sha1: &str, sha256: &str) -> Result<(PathBuf, PathBuf)> {
103        let sha1_path = self.sha1_path(sha1)?;
104        let (shard, suffix) = Self::shard_sha256(sha256)?;
105
106        let mut sha256_path = PathBuf::from("../../pkgs");
107        sha256_path.push(shard);
108        sha256_path.push(suffix);
109
110        Ok((sha1_path, sha256_path))
111    }
112
113    fn link_to_sha256(path: &Path) -> Result<String> {
114        let mut components = path.components().rev();
115
116        let tail = components.next().context("Link is missing filename")?;
117        let shard = components.next().context("Link is missing shard")?;
118
119        let tail = Self::component_to_name(&tail)?;
120        let shard = Self::component_to_name(&shard)?;
121
122        Ok(format!("{shard}{tail}"))
123    }
124
125    fn component_to_name<'a>(comp: &'a Component) -> Result<&'a str> {
126        let Component::Normal(comp) = comp else {
127            bail!("Component has reserved name")
128        };
129        let Some(comp) = comp.to_str() else {
130            bail!("Component is invalid utf8")
131        };
132        Ok(comp)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use std::path::Path;
140
141    #[test]
142    fn test_sha256_path() {
143        let dir = PkgsCacheDir {
144            path: PathBuf::from("/cache"),
145        };
146        assert!(dir.sha256_path("").is_err());
147        assert!(dir.sha256_path("ffff").is_err());
148        assert!(dir
149            .sha256_path("////////////////////////////////////////////////////////////////")
150            .is_err());
151
152        let path = dir
153            .sha256_path("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
154            .unwrap();
155        assert_eq!(
156            path,
157            Path::new("/cache/ff/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
158        );
159    }
160
161    #[test]
162    fn test_sha1_read_link() -> Result<()> {
163        let path = PkgsCacheDir::link_to_sha256(Path::new(
164            "../../../pkgs/ff/7951b5950a3a0319e86988041db4438b31a6ee4c7a36c64bd6c0c4607e40c9",
165        ))?;
166        assert_eq!(
167            path,
168            "ff7951b5950a3a0319e86988041db4438b31a6ee4c7a36c64bd6c0c4607e40c9"
169        );
170        Ok(())
171    }
172
173    #[test]
174    fn test_sha1_to_sha256() -> Result<()> {
175        let dir = PkgsCacheDir {
176            path: PathBuf::from("/cache"),
177        };
178
179        let (sha1, sha256) = dir.sha1_to_sha256(
180            "83d8ab27f4fd4725a147245f89d076aa96b52262",
181            "ff7951b5950a3a0319e86988041db4438b31a6ee4c7a36c64bd6c0c4607e40c9",
182        )?;
183        assert_eq!(
184            sha1,
185            Path::new("/cache/83/d8ab27f4fd4725a147245f89d076aa96b52262")
186        );
187        assert_eq!(
188            sha256,
189            Path::new(
190                "../../pkgs/ff/7951b5950a3a0319e86988041db4438b31a6ee4c7a36c64bd6c0c4607e40c9"
191            )
192        );
193
194        Ok(())
195    }
196}