1use std::fs::{self, File};
2use std::io::Write;
3use std::path::PathBuf;
4
5use sha2::{Digest, Sha256};
6
7use crate::error::{Error, IoResultExt, Result};
8use crate::hash::Hash;
9use crate::repo::Repo;
10use crate::types::Tree;
11
12pub fn write_tree(repo: &Repo, tree: &Tree) -> Result<Hash> {
17 let mut cbor_bytes = Vec::new();
19 ciborium::into_writer(tree, &mut cbor_bytes)?;
20
21 let compressed = zstd::encode_all(&cbor_bytes[..], 3).map_err(|e| Error::Io {
23 path: PathBuf::from("<zstd>"),
24 source: e,
25 })?;
26
27 let hash = Hash::from_bytes(Sha256::digest(&compressed).into());
29
30 let (dir, file) = hash.to_path_components();
31 let tree_dir = repo.trees_path().join(&dir);
32 let tree_path = tree_dir.join(&file);
33
34 if tree_path.exists() {
36 return Ok(hash);
37 }
38
39 fs::create_dir_all(&tree_dir).with_path(&tree_dir)?;
41
42 let tmp_path = repo.tmp_path().join(uuid::Uuid::new_v4().to_string());
44 {
45 let mut tmp_file = File::create(&tmp_path).with_path(&tmp_path)?;
46 tmp_file.write_all(&compressed).with_path(&tmp_path)?;
47 tmp_file.sync_all().with_path(&tmp_path)?;
48 }
49
50 fs::rename(&tmp_path, &tree_path).with_path(&tree_path)?;
52
53 let dir_file = File::open(&tree_dir).with_path(&tree_dir)?;
55 dir_file.sync_all().with_path(&tree_dir)?;
56
57 Ok(hash)
58}
59
60pub fn read_tree(repo: &Repo, hash: &Hash) -> Result<Tree> {
62 let path = tree_path(repo, hash);
63
64 let compressed = fs::read(&path).map_err(|e| {
65 if e.kind() == std::io::ErrorKind::NotFound {
66 Error::ObjectNotFound(*hash)
67 } else {
68 Error::Io {
69 path: path.clone(),
70 source: e,
71 }
72 }
73 })?;
74
75 let actual_hash = Hash::from_bytes(Sha256::digest(&compressed).into());
77 if actual_hash != *hash {
78 return Err(Error::CorruptObject(*hash));
79 }
80
81 let cbor_bytes = zstd::decode_all(&compressed[..]).map_err(|e| Error::Io {
83 path: path.clone(),
84 source: e,
85 })?;
86
87 let tree: Tree = ciborium::from_reader(&cbor_bytes[..])?;
89
90 Ok(tree)
91}
92
93pub fn tree_path(repo: &Repo, hash: &Hash) -> PathBuf {
95 let (dir, file) = hash.to_path_components();
96 repo.trees_path().join(dir).join(file)
97}
98
99#[allow(dead_code)]
101pub fn tree_exists(repo: &Repo, hash: &Hash) -> bool {
102 tree_path(repo, hash).exists()
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::types::{EntryKind, TreeEntry};
109 use tempfile::tempdir;
110
111 fn test_repo() -> (tempfile::TempDir, Repo) {
112 let dir = tempdir().unwrap();
113 let repo_path = dir.path().join("repo");
114 let repo = Repo::init(&repo_path).unwrap();
115 (dir, repo)
116 }
117
118 #[test]
119 fn test_write_and_read_tree() {
120 let (_dir, repo) = test_repo();
121
122 let entries = vec![
123 TreeEntry::new("file.txt", EntryKind::regular(Hash::ZERO, 100)),
124 TreeEntry::new("subdir", EntryKind::directory(Hash::ZERO, 0, 0, 0o755)),
125 ];
126 let tree = Tree::new(entries).unwrap();
127
128 let hash = write_tree(&repo, &tree).unwrap();
129 assert!(tree_exists(&repo, &hash));
130
131 let read_tree = read_tree(&repo, &hash).unwrap();
132 assert_eq!(tree, read_tree);
133 }
134
135 #[test]
136 fn test_tree_deduplication() {
137 let (_dir, repo) = test_repo();
138
139 let entries = vec![TreeEntry::new("foo", EntryKind::regular(Hash::ZERO, 50))];
140 let tree = Tree::new(entries).unwrap();
141
142 let h1 = write_tree(&repo, &tree).unwrap();
143 let h2 = write_tree(&repo, &tree).unwrap();
144
145 assert_eq!(h1, h2);
146 }
147
148 #[test]
149 fn test_empty_tree() {
150 let (_dir, repo) = test_repo();
151
152 let tree = Tree::empty();
153 let hash = write_tree(&repo, &tree).unwrap();
154
155 let read_tree = read_tree(&repo, &hash).unwrap();
156 assert!(read_tree.is_empty());
157 }
158
159 #[test]
160 fn test_read_nonexistent_tree() {
161 let (_dir, repo) = test_repo();
162
163 let fake_hash =
164 Hash::from_hex("1111111111111111111111111111111111111111111111111111111111111111")
165 .unwrap();
166 let result = read_tree(&repo, &fake_hash);
167
168 assert!(matches!(result, Err(Error::ObjectNotFound(_))));
169 }
170
171 #[test]
172 fn test_tree_with_all_entry_types() {
173 let (_dir, repo) = test_repo();
174
175 let entries = vec![
176 TreeEntry::new("regular", EntryKind::regular(Hash::ZERO, 100)),
177 TreeEntry::new("symlink", EntryKind::symlink(Hash::ZERO)),
178 TreeEntry::new("dir", EntryKind::directory(Hash::ZERO, 1000, 1000, 0o755)),
179 TreeEntry::new(
180 "block",
181 EntryKind::BlockDevice {
182 major: 8,
183 minor: 0,
184 uid: 0,
185 gid: 6,
186 mode: 0o660,
187 xattrs: vec![],
188 },
189 ),
190 TreeEntry::new(
191 "char",
192 EntryKind::CharDevice {
193 major: 1,
194 minor: 3,
195 uid: 0,
196 gid: 0,
197 mode: 0o666,
198 xattrs: vec![],
199 },
200 ),
201 TreeEntry::new(
202 "fifo",
203 EntryKind::Fifo {
204 uid: 0,
205 gid: 0,
206 mode: 0o644,
207 xattrs: vec![],
208 },
209 ),
210 TreeEntry::new("hardlink", EntryKind::hardlink("regular")),
211 ];
212
213 let tree = Tree::new(entries).unwrap();
214 let hash = write_tree(&repo, &tree).unwrap();
215
216 let read_tree = read_tree(&repo, &hash).unwrap();
217 assert_eq!(tree.len(), read_tree.len());
218
219 assert!(read_tree.get("regular").is_some());
221 assert!(read_tree.get("symlink").is_some());
222 assert!(read_tree.get("dir").is_some());
223 assert!(read_tree.get("block").is_some());
224 assert!(read_tree.get("char").is_some());
225 assert!(read_tree.get("fifo").is_some());
226 assert!(read_tree.get("hardlink").is_some());
227 }
228}