ic_host_fs/durable/export/
mod.rs1use crate::{identity::FileIdentity, path::canonicalize_allow_missing};
4use std::{
5 fs,
6 io::{self, Write},
7 path::{Path, PathBuf},
8};
9
10#[derive(Debug, thiserror::Error)]
15pub enum NonAliasingWriteError {
16 #[error("output {output:?} aliases protected path {protected:?}")]
18 Alias {
19 output: PathBuf,
21 protected: PathBuf,
23 },
24 #[error("export {path:?}: {source}")]
26 InvalidTarget {
27 path: PathBuf,
29 source: io::Error,
31 },
32 #[error("export {path:?}: {source}")]
34 CreateDirectory {
35 path: PathBuf,
37 source: io::Error,
39 },
40 #[error("export {path:?}: {source}")]
42 Write {
43 path: PathBuf,
45 source: io::Error,
47 },
48 #[error("export {path:?}: {source}")]
50 Sync {
51 path: PathBuf,
53 source: io::Error,
55 },
56}
57
58pub fn validate_non_aliasing(
72 output: &Path,
73 protected: &[&Path],
74 symlink_limit: usize,
75) -> Result<(), NonAliasingWriteError> {
76 let base = if output.is_relative() || protected.iter().any(|path| path.is_relative()) {
77 fs::canonicalize(".").map_err(|source| write_error(output, source))?
78 } else {
79 PathBuf::new()
80 };
81 let resolved = canonicalize_allow_missing(output, &base, symlink_limit)
82 .map_err(|source| write_error(output, source))?;
83 let output_metadata =
84 optional_metadata(output).map_err(|source| write_error(output, source))?;
85 for &path in protected {
86 let protected_resolved = canonicalize_allow_missing(path, &base, symlink_limit)
87 .map_err(|source| write_error(output, source))?;
88 let metadata = optional_metadata(path).map_err(|source| write_error(output, source))?;
89 if resolved == protected_resolved
90 || output_metadata
91 .as_ref()
92 .zip(metadata.as_ref())
93 .is_some_and(|(left, right)| {
94 FileIdentity::from_metadata(left) == FileIdentity::from_metadata(right)
95 })
96 {
97 return Err(alias_error(output, path));
98 }
99 }
100 Ok(())
101}
102
103pub fn write_non_aliasing(
119 output: &Path,
120 bytes: &[u8],
121 protected: &[&Path],
122 symlink_limit: usize,
123) -> Result<(), NonAliasingWriteError> {
124 validate_non_aliasing(output, protected, symlink_limit)?;
125 let parent = output
126 .parent()
127 .ok_or_else(|| NonAliasingWriteError::InvalidTarget {
128 path: output.to_path_buf(),
129 source: io::Error::new(
130 io::ErrorKind::InvalidInput,
131 "export target must have a parent",
132 ),
133 })?;
134 let parent = if parent.as_os_str().is_empty() {
135 Path::new(".")
136 } else {
137 parent
138 };
139 fs::create_dir_all(parent).map_err(|source| NonAliasingWriteError::CreateDirectory {
140 path: parent.to_path_buf(),
141 source,
142 })?;
143 let mut file = fs::OpenOptions::new()
144 .write(true)
145 .create(true)
146 .truncate(false)
147 .open(output)
148 .map_err(|source| write_error(output, source))?;
149 validate_non_aliasing(output, protected, symlink_limit)?;
150 write_opened(&mut file, output, bytes, protected, fs::File::sync_all)
151}
152
153fn write_opened(
154 file: &mut fs::File,
155 output: &Path,
156 bytes: &[u8],
157 protected: &[&Path],
158 sync: impl FnOnce(&fs::File) -> io::Result<()>,
159) -> Result<(), NonAliasingWriteError> {
160 let metadata = file
161 .metadata()
162 .map_err(|source| write_error(output, source))?;
163 for &path in protected {
164 if optional_metadata(path)
165 .map_err(|source| write_error(output, source))?
166 .is_some_and(|protected| {
167 FileIdentity::from_metadata(&metadata) == FileIdentity::from_metadata(&protected)
168 })
169 {
170 return Err(alias_error(output, path));
171 }
172 }
173 file.set_len(0)
174 .map_err(|source| write_error(output, source))?;
175 file.write_all(bytes)
176 .map_err(|source| write_error(output, source))?;
177 sync(file).map_err(|source| NonAliasingWriteError::Sync {
178 path: output.to_path_buf(),
179 source,
180 })
181}
182
183fn optional_metadata(path: &Path) -> io::Result<Option<fs::Metadata>> {
184 match fs::metadata(path) {
185 Ok(metadata) => Ok(Some(metadata)),
186 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
187 Err(error) => Err(error),
188 }
189}
190
191fn alias_error(output: &Path, protected: &Path) -> NonAliasingWriteError {
192 NonAliasingWriteError::Alias {
193 output: output.to_path_buf(),
194 protected: protected.to_path_buf(),
195 }
196}
197
198fn write_error(path: &Path, source: io::Error) -> NonAliasingWriteError {
199 NonAliasingWriteError::Write {
200 path: path.to_path_buf(),
201 source,
202 }
203}
204
205#[cfg(test)]
206mod tests;