use crate::{identity::FileIdentity, path::canonicalize_allow_missing};
use std::{
fs,
io::{self, Write},
path::{Path, PathBuf},
};
#[derive(Debug, thiserror::Error)]
pub enum NonAliasingWriteError {
#[error("output {output:?} aliases protected path {protected:?}")]
Alias {
output: PathBuf,
protected: PathBuf,
},
#[error("export {path:?}: {source}")]
InvalidTarget {
path: PathBuf,
source: io::Error,
},
#[error("export {path:?}: {source}")]
CreateDirectory {
path: PathBuf,
source: io::Error,
},
#[error("export {path:?}: {source}")]
Write {
path: PathBuf,
source: io::Error,
},
#[error("export {path:?}: {source}")]
Sync {
path: PathBuf,
source: io::Error,
},
}
pub fn validate_non_aliasing(
output: &Path,
protected: &[&Path],
symlink_limit: usize,
) -> Result<(), NonAliasingWriteError> {
let base = if output.is_relative() || protected.iter().any(|path| path.is_relative()) {
fs::canonicalize(".").map_err(|source| write_error(output, source))?
} else {
PathBuf::new()
};
let resolved = canonicalize_allow_missing(output, &base, symlink_limit)
.map_err(|source| write_error(output, source))?;
let output_metadata =
optional_metadata(output).map_err(|source| write_error(output, source))?;
for &path in protected {
let protected_resolved = canonicalize_allow_missing(path, &base, symlink_limit)
.map_err(|source| write_error(output, source))?;
let metadata = optional_metadata(path).map_err(|source| write_error(output, source))?;
if resolved == protected_resolved
|| output_metadata
.as_ref()
.zip(metadata.as_ref())
.is_some_and(|(left, right)| {
FileIdentity::from_metadata(left) == FileIdentity::from_metadata(right)
})
{
return Err(alias_error(output, path));
}
}
Ok(())
}
pub fn write_non_aliasing(
output: &Path,
bytes: &[u8],
protected: &[&Path],
symlink_limit: usize,
) -> Result<(), NonAliasingWriteError> {
validate_non_aliasing(output, protected, symlink_limit)?;
let parent = output
.parent()
.ok_or_else(|| NonAliasingWriteError::InvalidTarget {
path: output.to_path_buf(),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"export target must have a parent",
),
})?;
let parent = if parent.as_os_str().is_empty() {
Path::new(".")
} else {
parent
};
fs::create_dir_all(parent).map_err(|source| NonAliasingWriteError::CreateDirectory {
path: parent.to_path_buf(),
source,
})?;
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(output)
.map_err(|source| write_error(output, source))?;
validate_non_aliasing(output, protected, symlink_limit)?;
write_opened(&mut file, output, bytes, protected, fs::File::sync_all)
}
fn write_opened(
file: &mut fs::File,
output: &Path,
bytes: &[u8],
protected: &[&Path],
sync: impl FnOnce(&fs::File) -> io::Result<()>,
) -> Result<(), NonAliasingWriteError> {
let metadata = file
.metadata()
.map_err(|source| write_error(output, source))?;
for &path in protected {
if optional_metadata(path)
.map_err(|source| write_error(output, source))?
.is_some_and(|protected| {
FileIdentity::from_metadata(&metadata) == FileIdentity::from_metadata(&protected)
})
{
return Err(alias_error(output, path));
}
}
file.set_len(0)
.map_err(|source| write_error(output, source))?;
file.write_all(bytes)
.map_err(|source| write_error(output, source))?;
sync(file).map_err(|source| NonAliasingWriteError::Sync {
path: output.to_path_buf(),
source,
})
}
fn optional_metadata(path: &Path) -> io::Result<Option<fs::Metadata>> {
match fs::metadata(path) {
Ok(metadata) => Ok(Some(metadata)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
}
}
fn alias_error(output: &Path, protected: &Path) -> NonAliasingWriteError {
NonAliasingWriteError::Alias {
output: output.to_path_buf(),
protected: protected.to_path_buf(),
}
}
fn write_error(path: &Path, source: io::Error) -> NonAliasingWriteError {
NonAliasingWriteError::Write {
path: path.to_path_buf(),
source,
}
}
#[cfg(test)]
mod tests;