1use std::path::{Path, PathBuf};
2
3use sha2::{Digest, Sha256};
4
5use crate::error::{Result, TuffError};
6
7pub fn cache_root(home: &Path) -> PathBuf {
8 crate::paths::user_cache(home).join("sha256")
9}
10
11pub fn cache_path(home: &Path, hash: &str) -> Result<PathBuf> {
12 validate_hash(hash)?;
13 Ok(cache_root(home).join(&hash[..2]).join(hash))
14}
15
16pub fn validate_hash(hash: &str) -> Result<()> {
17 if hash.len() != 64 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
18 return Err(TuffError::new(format!("invalid capability hash: {hash}")));
19 }
20 Ok(())
21}
22
23pub fn hash_tree(root: &Path) -> Result<String> {
26 let mut files = Vec::new();
27 collect_files(root, root, &mut files)?;
28 files.sort_by(|a, b| a.0.cmp(&b.0));
29
30 let mut hasher = Sha256::new();
31 for (path, content) in files {
32 let path = path.to_string_lossy().replace('\\', "/");
33 hasher.update((path.len() as u64).to_be_bytes());
34 hasher.update(path.as_bytes());
35 hasher.update((content.len() as u64).to_be_bytes());
36 hasher.update(content);
37 }
38 Ok(format!("{:x}", hasher.finalize()))
39}
40
41pub fn populate(home: &Path, hash: &str, source: &Path) -> Result<PathBuf> {
42 let destination = cache_path(home, hash)?;
43 let actual = hash_tree(source)?;
44 if actual != hash {
45 return Err(TuffError::new(format!(
46 "materialized capability hash mismatch: expected {hash}, got {actual}"
47 )));
48 }
49
50 if destination.is_dir() {
51 if hash_tree(&destination)? == hash {
52 return Ok(destination);
53 }
54 if let Err(error) = std::fs::remove_dir_all(&destination) {
55 if error.kind() == std::io::ErrorKind::PermissionDenied {
56 return Ok(source.to_path_buf());
57 }
58 return Err(error.into());
59 }
60 }
61
62 let parent = destination
63 .parent()
64 .ok_or_else(|| TuffError::new("invalid cache destination"))?;
65 if let Err(error) = std::fs::create_dir_all(parent) {
66 if error.kind() == std::io::ErrorKind::PermissionDenied {
67 return Ok(source.to_path_buf());
68 }
69 return Err(error.into());
70 }
71 let temporary = match tempfile::Builder::new()
72 .prefix("tuff-cache-")
73 .tempdir_in(parent)
74 {
75 Ok(temporary) => temporary,
76 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
77 return Ok(source.to_path_buf());
78 }
79 Err(error) => return Err(error.into()),
80 };
81 if let Err(error) = copy_tree(source, temporary.path()) {
82 if error.to_string().contains("Permission denied") {
83 return Ok(source.to_path_buf());
84 }
85 return Err(error);
86 }
87 let temporary_path = temporary.keep();
88 if let Err(error) = std::fs::rename(&temporary_path, &destination) {
89 if error.kind() == std::io::ErrorKind::PermissionDenied {
90 return Ok(source.to_path_buf());
91 }
92 return Err(error.into());
93 }
94 Ok(destination)
95}
96
97pub fn read_verified(home: &Path, hash: &str) -> Result<Option<PathBuf>> {
98 let path = cache_path(home, hash)?;
99 if !path.is_dir() {
100 return Ok(None);
101 }
102 if hash_tree(&path)? != hash {
103 return Ok(None);
104 }
105 Ok(Some(path))
106}
107
108pub fn clear(home: &Path) -> Result<()> {
109 let root = crate::paths::user_cache(home);
110 if root.exists() {
111 std::fs::remove_dir_all(root)?;
112 }
113 Ok(())
114}
115
116fn collect_files(root: &Path, current: &Path, output: &mut Vec<(PathBuf, Vec<u8>)>) -> Result<()> {
117 for entry in std::fs::read_dir(current)? {
118 let entry = entry?;
119 let path = entry.path();
120 if path.is_dir() {
121 collect_files(root, &path, output)?;
122 } else if path.is_file() {
123 let relative = path
124 .strip_prefix(root)
125 .map_err(|error| TuffError::new(error.to_string()))?
126 .to_path_buf();
127 output.push((relative, std::fs::read(path)?));
128 }
129 }
130 Ok(())
131}
132
133fn copy_tree(source: &Path, destination: &Path) -> Result<()> {
134 std::fs::create_dir_all(destination)?;
135 for entry in std::fs::read_dir(source)? {
136 let entry = entry?;
137 let source_path = entry.path();
138 let destination_path = destination.join(entry.file_name());
139 if source_path.is_dir() {
140 copy_tree(&source_path, &destination_path)?;
141 } else if source_path.is_file() {
142 std::fs::copy(source_path, destination_path)?;
143 }
144 }
145 Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use tempfile::TempDir;
152
153 #[test]
154 fn hash_tree_is_independent_of_creation_order() {
155 let left = TempDir::new().unwrap();
156 let right = TempDir::new().unwrap();
157 std::fs::create_dir_all(left.path().join("nested")).unwrap();
158 std::fs::create_dir_all(right.path().join("nested")).unwrap();
159 std::fs::write(left.path().join("a"), "a").unwrap();
160 std::fs::write(left.path().join("nested/b"), "b").unwrap();
161 std::fs::write(right.path().join("nested/b"), "b").unwrap();
162 std::fs::write(right.path().join("a"), "a").unwrap();
163 assert_eq!(
164 hash_tree(left.path()).unwrap(),
165 hash_tree(right.path()).unwrap()
166 );
167 }
168
169 #[test]
170 fn cache_round_trip_verifies_content() {
171 let home = TempDir::new().unwrap();
172 let source = TempDir::new().unwrap();
173 std::fs::write(source.path().join("file"), "content").unwrap();
174 let hash = hash_tree(source.path()).unwrap();
175 let path = populate(home.path(), &hash, source.path()).unwrap();
176 assert_eq!(read_verified(home.path(), &hash).unwrap(), Some(path));
177 }
178}