1use std::io::Read as _;
4use std::path::{Path, PathBuf};
5
6use aws_lc_rs::digest::{Context, SHA256};
7use yapi_types::time::hex;
8
9use super::{ChestError, invalid, io};
10
11pub fn tree_hash(root: &Path) -> Result<String, ChestError> {
22 let metadata = root.symlink_metadata().map_err(io(root))?;
23 if metadata.is_symlink() {
24 return Err(refused(root));
25 }
26 let mut files = Vec::new();
27 if metadata.is_dir() {
28 collect(root, "", &mut files)?;
29 } else {
30 files.push((String::new(), root.to_path_buf()));
31 }
32 files.sort();
33 let mut context = Context::new(&SHA256);
34 let mut buffer = vec![0; 64 * 1024];
35 for (relative, path) in files {
36 let metadata = path.symlink_metadata().map_err(io(&path))?;
37 context.update(relative.as_bytes());
38 context.update(b"\0");
39 context.update(if executable(&metadata) { b"x" } else { b"-" });
40 let mut file = std::fs::File::open(&path).map_err(io(&path))?;
44 let mut contents = Context::new(&SHA256);
45 loop {
46 match file.read(&mut buffer) {
47 Ok(0) => break,
48 Ok(read) => contents.update(&buffer[..read]),
49 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
50 Err(err) => return Err(io(&path)(err)),
51 }
52 }
53 context.update(contents.finish().as_ref());
54 }
55 Ok(format!("sha256-{}", hex(context.finish().as_ref())))
56}
57
58fn refused(path: &Path) -> ChestError {
59 invalid(format!(
60 "{}: symbolic links are not allowed in a package",
61 path.display()
62 ))
63}
64
65fn collect(dir: &Path, prefix: &str, files: &mut Vec<(String, PathBuf)>) -> Result<(), ChestError> {
68 for entry in std::fs::read_dir(dir).map_err(io(dir))? {
69 let entry = entry.map_err(io(dir))?;
70 let path = entry.path();
71 let kind = entry.file_type().map_err(io(&path))?;
72 let name = entry
73 .file_name()
74 .into_string()
75 .map_err(|_| invalid(format!("{}: the name is not UTF-8", path.display())))?;
76 if name == ".git" {
78 continue;
79 }
80 let relative = if prefix.is_empty() {
81 name.clone()
82 } else {
83 format!("{prefix}/{name}")
84 };
85 if kind.is_symlink() {
86 return Err(refused(&path));
87 } else if kind.is_dir() {
88 collect(&path, &relative, files)?;
89 } else if kind.is_file() {
90 files.push((relative, path));
91 }
92 }
93 Ok(())
94}
95
96#[cfg(unix)]
97fn executable(metadata: &std::fs::Metadata) -> bool {
98 use std::os::unix::fs::PermissionsExt as _;
99 metadata.permissions().mode() & 0o111 != 0
100}
101
102#[cfg(not(unix))]
103fn executable(_: &std::fs::Metadata) -> bool {
104 false
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn scratch(name: &str) -> PathBuf {
112 let dir =
113 std::env::temp_dir().join(format!("yapi-chest-hash-{name}-{}", std::process::id()));
114 let _ = std::fs::remove_dir_all(&dir);
115 std::fs::create_dir_all(&dir).unwrap();
116 dir
117 }
118
119 fn write(root: &Path, path: &str, contents: &str) {
120 let path = root.join(path);
121 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
122 std::fs::write(path, contents).unwrap();
123 }
124
125 #[cfg(unix)]
126 fn chmod(root: &Path, path: &str, mode: u32) {
127 use std::os::unix::fs::PermissionsExt as _;
128 std::fs::set_permissions(root.join(path), std::fs::Permissions::from_mode(mode)).unwrap();
129 }
130
131 #[cfg(unix)]
132 #[test]
133 fn tree_hash_vector() {
134 let root = scratch("vector");
135 write(&root, "a.txt", "hello\n");
136 write(&root, "bin/run", "#!/bin/sh\n");
137 chmod(&root, "a.txt", 0o644);
138 chmod(&root, "bin/run", 0o755);
139 let vector = "sha256-8ec032806587aaad014b497c7476c1dd2321f8c3acc4e7b03c45101290abc097";
144 assert_eq!(tree_hash(&root).unwrap(), vector);
145
146 write(&root, ".git/HEAD", "ref: refs/heads/main\n");
149 write(&root, "bin/.git/config", "[core]\n");
150 write(&root, "sub/.git", "gitdir: ../.git/modules/sub\n");
151 assert_eq!(tree_hash(&root).unwrap(), vector);
152 chmod(&root, "a.txt", 0o654);
154 assert_ne!(tree_hash(&root).unwrap(), vector);
155 chmod(&root, "a.txt", 0o644);
156 std::os::unix::fs::symlink("a.txt", root.join("bin/link")).unwrap();
158 let error = tree_hash(&root).unwrap_err().to_string();
159 assert!(
160 error.ends_with("bin/link: symbolic links are not allowed in a package"),
161 "{error}"
162 );
163 std::fs::remove_file(root.join("bin/link")).unwrap();
164 let link = scratch("vector-link");
165 std::fs::remove_dir(&link).unwrap();
166 std::os::unix::fs::symlink(&root, &link).unwrap();
167 assert!(tree_hash(&link).is_err());
168
169 assert_eq!(
172 tree_hash(&root.join("a.txt")).unwrap(),
173 "sha256-89c31f91aca023cafd6de1c6c60492fe1c63117041231bdf5370629f71254cec"
174 );
175
176 let (one, two) = (scratch("ab-c"), scratch("a-bc"));
178 write(&one, "ab", "c");
179 write(&two, "a", "bc");
180 assert_ne!(tree_hash(&one).unwrap(), tree_hash(&two).unwrap());
181 for dir in [root, link, one, two] {
182 let _ = std::fs::remove_dir_all(&dir);
183 let _ = std::fs::remove_file(&dir);
184 }
185 }
186}