ic_host_fs/read/tree/checksum/
mod.rs1use super::{TreeVisitor, walk_tree};
7use crate::read::{OpenEntryError, OpenedEntry, open_directory_no_follow, open_entry_beneath};
8use ic_host_artifacts::artifact::{
9 ArtifactError, Sha256Digest, hash_reader,
10 tree::{DirectoryChecksumError, checksum_relative_files},
11};
12use rustix::{
13 fd::{AsFd, OwnedFd},
14 fs::FileType,
15};
16use std::{
17 fs::File,
18 io,
19 path::{Component, Path, PathBuf},
20};
21
22pub fn checksum_directory(
34 path: &Path,
35 max_file_bytes: u64,
36) -> Result<Sha256Digest, TreeChecksumError> {
37 let (root, relative) = if path.is_absolute() {
38 (
39 Path::new("/"),
40 path.strip_prefix("/").map_err(io::Error::other)?,
41 )
42 } else {
43 (Path::new("."), path)
44 };
45 if relative.as_os_str().is_empty()
46 || !relative
47 .components()
48 .all(|part| matches!(part, Component::Normal(name) if name.to_str().is_some()))
49 {
50 return Err(TreeChecksumError::InvalidPath {
51 path: path.to_path_buf(),
52 });
53 }
54 let root = open_directory_no_follow(root)?;
55 let directory = match open_entry_beneath(root.as_fd(), relative) {
56 Ok(OpenedEntry::Directory(file)) => OwnedFd::from(file),
57 Ok(OpenedEntry::RegularFile(_)) => {
58 return Err(TreeChecksumError::UnsupportedEntry {
59 path: path.to_path_buf(),
60 kind: FileType::RegularFile,
61 });
62 }
63 Err(OpenEntryError::UnsupportedEntry { kind }) => {
64 return Err(TreeChecksumError::UnsupportedEntry {
65 path: path.to_path_buf(),
66 kind,
67 });
68 }
69 Err(OpenEntryError::Io(error)) => return Err(error.into()),
70 };
71 checksum_directory_at(&directory, path, max_file_bytes)
72}
73
74pub fn checksum_directory_at(
84 directory: &OwnedFd,
85 display_root: &Path,
86 max_file_bytes: u64,
87) -> Result<Sha256Digest, TreeChecksumError> {
88 let mut visitor = ChecksumVisitor::new(max_file_bytes);
89 walk_tree(directory, Path::new(""), display_root, &mut visitor)?;
90 visitor.finish()
91}
92
93pub(crate) struct ChecksumVisitor {
94 checksums: Vec<(PathBuf, Sha256Digest)>,
95 max_file_bytes: u64,
96}
97impl ChecksumVisitor {
98 pub(crate) const fn new(max_file_bytes: u64) -> Self {
99 Self {
100 checksums: Vec::new(),
101 max_file_bytes,
102 }
103 }
104
105 pub(crate) fn record_file(
106 &mut self,
107 file: File,
108 relative: &Path,
109 ) -> Result<(), TreeChecksumError> {
110 let identity = hash_reader(file, self.max_file_bytes).map_err(TreeChecksumError::Input)?;
111 self.checksums
112 .push((relative.to_path_buf(), identity.sha256));
113 Ok(())
114 }
115
116 pub(crate) fn finish(self) -> Result<Sha256Digest, TreeChecksumError> {
117 checksum_relative_files(self.checksums).map_err(TreeChecksumError::Declaration)
118 }
119}
120
121impl TreeVisitor for ChecksumVisitor {
122 type Error = TreeChecksumError;
123 fn file(&mut self, fd: &OwnedFd, relative: &Path, _display: &Path) -> Result<(), Self::Error> {
124 self.record_file(File::from(fd.try_clone()?), relative)
125 }
126 fn enter_directory(&mut self, _relative: &Path, _display: &Path) -> Result<(), Self::Error> {
127 Ok(())
128 }
129 fn leave_directory(
130 &mut self,
131 _fd: &OwnedFd,
132 _relative: &Path,
133 _display: &Path,
134 ) -> Result<(), Self::Error> {
135 Ok(())
136 }
137 fn unsupported(&self, display: &Path, kind: FileType) -> Self::Error {
138 TreeChecksumError::UnsupportedEntry {
139 path: display.to_path_buf(),
140 kind,
141 }
142 }
143 fn non_utf8(&self, display: &Path) -> Self::Error {
144 TreeChecksumError::NonUtf8Path {
145 path: display.to_path_buf(),
146 }
147 }
148}
149
150#[derive(Debug, thiserror::Error)]
152pub enum TreeChecksumError {
153 #[error("{0}")]
155 Io(#[from] io::Error),
156 #[error("{0}")]
158 Input(#[from] ArtifactError),
159 #[error("{0}")]
161 Declaration(#[from] DirectoryChecksumError),
162 #[error("invalid directory checksum path: {path:?}")]
164 InvalidPath {
165 path: PathBuf,
167 },
168 #[error("directory checksum path is not UTF-8: {path:?}")]
170 NonUtf8Path {
171 path: PathBuf,
173 },
174 #[error("unsupported directory checksum entry at {path:?}: {kind:?}")]
176 UnsupportedEntry {
177 path: PathBuf,
179 kind: FileType,
181 },
182}
183
184#[cfg(test)]
185mod tests;