1use std::fs::{self, File};
2use std::io::Write;
3use std::path::PathBuf;
4
5use crate::error::{Error, IoResultExt, Result};
6use crate::hash::Hash;
7use crate::repo::Repo;
8
9pub fn write_ref(repo: &Repo, ref_name: &str, hash: &Hash) -> Result<()> {
13 validate_ref_name(ref_name)?;
14
15 let ref_path = ref_path(repo, ref_name);
16
17 if let Some(parent) = ref_path.parent() {
19 fs::create_dir_all(parent).with_path(parent)?;
20 }
21
22 let tmp_path = repo.tmp_path().join(uuid::Uuid::new_v4().to_string());
24 {
25 let mut tmp_file = File::create(&tmp_path).with_path(&tmp_path)?;
26 writeln!(tmp_file, "{}", hash.to_hex()).with_path(&tmp_path)?;
27 tmp_file.sync_all().with_path(&tmp_path)?;
28 }
29
30 fs::rename(&tmp_path, &ref_path).with_path(&ref_path)?;
32
33 if let Some(parent) = ref_path.parent() {
35 let dir = File::open(parent).with_path(parent)?;
36 dir.sync_all().with_path(parent)?;
37 }
38
39 Ok(())
40}
41
42pub fn read_ref(repo: &Repo, ref_name: &str) -> Result<Hash> {
44 let ref_path = ref_path(repo, ref_name);
45
46 let content = fs::read_to_string(&ref_path).map_err(|e| {
47 if e.kind() == std::io::ErrorKind::NotFound {
48 Error::RefNotFound(ref_name.to_string())
49 } else {
50 Error::Io {
51 path: ref_path.clone(),
52 source: e,
53 }
54 }
55 })?;
56
57 let hex = content.trim();
58 Hash::from_hex(hex)
59}
60
61pub fn delete_ref(repo: &Repo, ref_name: &str) -> Result<()> {
63 let ref_path = ref_path(repo, ref_name);
64
65 fs::remove_file(&ref_path).map_err(|e| {
66 if e.kind() == std::io::ErrorKind::NotFound {
67 Error::RefNotFound(ref_name.to_string())
68 } else {
69 Error::Io {
70 path: ref_path,
71 source: e,
72 }
73 }
74 })
75}
76
77pub fn delete_refs_matching(repo: &Repo, pattern: &str) -> Result<Vec<String>> {
79 let matching = list_refs_matching(repo, pattern)?;
80 for ref_name in &matching {
81 delete_ref(repo, ref_name)?;
82 }
83 Ok(matching)
84}
85
86pub fn resolve_ref(repo: &Repo, ref_or_hash: &str) -> Result<Hash> {
91 if ref_or_hash.len() == 64 && ref_or_hash.chars().all(|c| c.is_ascii_hexdigit()) {
93 return Hash::from_hex(ref_or_hash);
94 }
95
96 read_ref(repo, ref_or_hash)
98}
99
100pub fn list_refs(repo: &Repo) -> Result<Vec<String>> {
102 let refs_dir = repo.refs_path();
103 let mut refs = Vec::new();
104
105 if refs_dir.exists() {
106 collect_refs(&refs_dir, &refs_dir, &mut refs)?;
107 }
108
109 refs.sort();
110 Ok(refs)
111}
112
113pub fn list_refs_matching(repo: &Repo, pattern: &str) -> Result<Vec<String>> {
115 let all_refs = list_refs(repo)?;
116 let glob = glob::Pattern::new(pattern).map_err(|e| Error::InvalidRef(e.to_string()))?;
117
118 Ok(all_refs.into_iter().filter(|r| glob.matches(r)).collect())
119}
120
121pub fn ref_exists(repo: &Repo, ref_name: &str) -> bool {
123 ref_path(repo, ref_name).exists()
124}
125
126fn ref_path(repo: &Repo, ref_name: &str) -> PathBuf {
128 repo.refs_path().join(ref_name)
129}
130
131fn collect_refs(base: &PathBuf, dir: &PathBuf, refs: &mut Vec<String>) -> Result<()> {
133 for entry in fs::read_dir(dir).with_path(dir)? {
134 let entry = entry.with_path(dir)?;
135 let path = entry.path();
136
137 if path.is_dir() {
138 collect_refs(base, &path, refs)?;
139 } else if path.is_file() {
140 if let Ok(rel) = path.strip_prefix(base) {
142 let ref_name = rel.to_string_lossy().to_string();
143 refs.push(ref_name);
144 }
145 }
146 }
147 Ok(())
148}
149
150fn validate_ref_name(name: &str) -> Result<()> {
152 if name.is_empty() {
153 return Err(Error::InvalidRef("empty ref name".to_string()));
154 }
155
156 if name.starts_with('/') || name.ends_with('/') {
157 return Err(Error::InvalidRef(format!(
158 "ref name cannot start or end with '/': {}",
159 name
160 )));
161 }
162
163 if name.contains("//") {
164 return Err(Error::InvalidRef(format!(
165 "ref name cannot contain '//': {}",
166 name
167 )));
168 }
169
170 if name.contains('\0') {
171 return Err(Error::InvalidRef(format!(
172 "ref name cannot contain null byte: {}",
173 name
174 )));
175 }
176
177 for component in name.split('/') {
179 if component == "." || component == ".." {
180 return Err(Error::InvalidRef(format!(
181 "ref name cannot contain '.' or '..': {}",
182 name
183 )));
184 }
185 }
186
187 Ok(())
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use tempfile::tempdir;
194
195 fn test_repo() -> (tempfile::TempDir, Repo) {
196 let dir = tempdir().unwrap();
197 let repo_path = dir.path().join("repo");
198 let repo = Repo::init(&repo_path).unwrap();
199 (dir, repo)
200 }
201
202 #[test]
203 fn test_write_and_read_ref() {
204 let (_dir, repo) = test_repo();
205
206 let hash =
207 Hash::from_hex("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
208 .unwrap();
209
210 write_ref(&repo, "test/ref", &hash).unwrap();
211 let read_hash = read_ref(&repo, "test/ref").unwrap();
212
213 assert_eq!(hash, read_hash);
214 }
215
216 #[test]
217 fn test_hierarchical_ref() {
218 let (_dir, repo) = test_repo();
219
220 let hash = Hash::ZERO;
221 write_ref(&repo, "x86_64/pkg/bzip2/1.0.8/outputs/bin", &hash).unwrap();
222
223 let read_hash = read_ref(&repo, "x86_64/pkg/bzip2/1.0.8/outputs/bin").unwrap();
224 assert_eq!(hash, read_hash);
225 }
226
227 #[test]
228 fn test_delete_ref() {
229 let (_dir, repo) = test_repo();
230
231 let hash = Hash::ZERO;
232 write_ref(&repo, "test/ref", &hash).unwrap();
233 assert!(ref_exists(&repo, "test/ref"));
234
235 delete_ref(&repo, "test/ref").unwrap();
236 assert!(!ref_exists(&repo, "test/ref"));
237 }
238
239 #[test]
240 fn test_delete_nonexistent_ref() {
241 let (_dir, repo) = test_repo();
242
243 let result = delete_ref(&repo, "nonexistent");
244 assert!(matches!(result, Err(Error::RefNotFound(_))));
245 }
246
247 #[test]
248 fn test_read_nonexistent_ref() {
249 let (_dir, repo) = test_repo();
250
251 let result = read_ref(&repo, "nonexistent");
252 assert!(matches!(result, Err(Error::RefNotFound(_))));
253 }
254
255 #[test]
256 fn test_list_refs() {
257 let (_dir, repo) = test_repo();
258
259 write_ref(&repo, "a/b/c", &Hash::ZERO).unwrap();
260 write_ref(&repo, "x/y", &Hash::ZERO).unwrap();
261 write_ref(&repo, "single", &Hash::ZERO).unwrap();
262
263 let refs = list_refs(&repo).unwrap();
264 assert_eq!(refs.len(), 3);
265 assert!(refs.contains(&"a/b/c".to_string()));
266 assert!(refs.contains(&"x/y".to_string()));
267 assert!(refs.contains(&"single".to_string()));
268 }
269
270 #[test]
271 fn test_list_refs_matching() {
272 let (_dir, repo) = test_repo();
273
274 write_ref(&repo, "x86_64/pkg/foo/1.0", &Hash::ZERO).unwrap();
275 write_ref(&repo, "x86_64/pkg/bar/2.0", &Hash::ZERO).unwrap();
276 write_ref(&repo, "aarch64/pkg/foo/1.0", &Hash::ZERO).unwrap();
277
278 let refs = list_refs_matching(&repo, "x86_64/*").unwrap();
279 assert_eq!(refs.len(), 2);
280
281 let refs = list_refs_matching(&repo, "*/pkg/foo/*").unwrap();
282 assert_eq!(refs.len(), 2);
283 }
284
285 #[test]
286 fn test_resolve_ref_hash() {
287 let (_dir, repo) = test_repo();
288
289 let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
291 let hash = resolve_ref(&repo, hex).unwrap();
292 assert_eq!(hash.to_hex(), hex);
293 }
294
295 #[test]
296 fn test_resolve_ref_name() {
297 let (_dir, repo) = test_repo();
298
299 let hash =
300 Hash::from_hex("1111111111111111111111111111111111111111111111111111111111111111")
301 .unwrap();
302 write_ref(&repo, "myref", &hash).unwrap();
303
304 let resolved = resolve_ref(&repo, "myref").unwrap();
305 assert_eq!(resolved, hash);
306 }
307
308 #[test]
309 fn test_invalid_ref_names() {
310 assert!(validate_ref_name("").is_err());
311 assert!(validate_ref_name("/start").is_err());
312 assert!(validate_ref_name("end/").is_err());
313 assert!(validate_ref_name("double//slash").is_err());
314 assert!(validate_ref_name("with/./dot").is_err());
315 assert!(validate_ref_name("with/../dotdot").is_err());
316 assert!(validate_ref_name("with\0null").is_err());
317
318 assert!(validate_ref_name("simple").is_ok());
320 assert!(validate_ref_name("with/slash").is_ok());
321 assert!(validate_ref_name("deep/nested/path/ref").is_ok());
322 }
323
324 #[test]
325 fn test_overwrite_ref() {
326 let (_dir, repo) = test_repo();
327
328 let hash1 =
329 Hash::from_hex("1111111111111111111111111111111111111111111111111111111111111111")
330 .unwrap();
331 let hash2 =
332 Hash::from_hex("2222222222222222222222222222222222222222222222222222222222222222")
333 .unwrap();
334
335 write_ref(&repo, "myref", &hash1).unwrap();
336 write_ref(&repo, "myref", &hash2).unwrap();
337
338 let read_hash = read_ref(&repo, "myref").unwrap();
339 assert_eq!(read_hash, hash2);
340 }
341}