use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use std::str::FromStr;
use walkdir::WalkDir;
use crate::{Bundle, BundleError, BundleRelativePath, Manifest, SecretRef};
const MANIFEST: &str = "manifest.yaml";
const SECRETS: &str = "secrets.ref.yaml";
impl Bundle {
pub fn write_directory(&self, output: &Path) -> Result<(), BundleError> {
self.verify()?;
if output.exists() {
return Err(BundleError::OutputExists(output.to_path_buf()));
}
let parent = usable_parent(output);
fs::create_dir_all(parent).map_err(|source| io_error(parent, source))?;
let temporary = tempfile::tempdir_in(parent).map_err(|source| io_error(parent, source))?;
write_file(
&temporary.path().join(MANIFEST),
serde_yaml::to_string(self.manifest())?.as_bytes(),
0o600,
)?;
write_file(
&temporary.path().join(SECRETS),
serde_yaml::to_string(&self.manifest().secret_references)?.as_bytes(),
0o600,
)?;
for (path, content) in self.files() {
let record = self
.manifest()
.files
.get(path)
.ok_or_else(|| BundleError::MissingFile(path.clone()))?;
write_file(&temporary.path().join(path.as_path()), content, record.mode)?;
}
let staged = temporary.keep();
fs::rename(&staged, output).map_err(|source| io_error(output, source))
}
pub fn from_directory(input: &Path) -> Result<Self, BundleError> {
reject_root_symlink(input)?;
let manifest: Manifest = serde_yaml::from_slice(&read(&input.join(MANIFEST))?)?;
let references: Vec<SecretRef> = serde_yaml::from_slice(&read(&input.join(SECRETS))?)?;
if references != manifest.secret_references {
return Err(BundleError::SecretReferenceMismatch);
}
let mut content = BTreeMap::new();
for (path, record) in &manifest.files {
let disk_path = input.join(path.as_path());
reject_components(input, path.as_path())?;
verify_mode(&disk_path, record.mode)?;
content.insert(path.clone(), read(&disk_path)?);
}
reject_undeclared(input, &manifest)?;
Self::from_parts(manifest, content)
}
}
fn write_file(path: &Path, content: &[u8], mode: u32) -> Result<(), BundleError> {
let parent = path
.parent()
.ok_or_else(|| io_error(path, std::io::Error::other("missing parent")))?;
fs::create_dir_all(parent).map_err(|source| io_error(parent, source))?;
fs::write(path, content).map_err(|source| io_error(path, source))?;
set_mode(path, mode)
}
fn read(path: &Path) -> Result<Vec<u8>, BundleError> {
fs::read(path).map_err(|source| io_error(path, source))
}
fn reject_root_symlink(input: &Path) -> Result<(), BundleError> {
let metadata = fs::symlink_metadata(input).map_err(|source| io_error(input, source))?;
if metadata.file_type().is_symlink() {
return Err(BundleError::DirectorySymlink(input.to_path_buf()));
}
Ok(())
}
fn reject_components(root: &Path, relative: &Path) -> Result<(), BundleError> {
let mut current = root.to_path_buf();
for component in relative.components() {
current.push(component);
let metadata =
fs::symlink_metadata(¤t).map_err(|source| io_error(¤t, source))?;
if metadata.file_type().is_symlink() {
return Err(BundleError::DirectorySymlink(current));
}
}
Ok(())
}
fn reject_undeclared(root: &Path, manifest: &Manifest) -> Result<(), BundleError> {
for item in WalkDir::new(root).follow_links(false).min_depth(1) {
let entry = item.map_err(|error| {
let path = error
.path()
.map_or_else(|| root.to_path_buf(), Path::to_path_buf);
io_error(&path, std::io::Error::other(error.to_string()))
})?;
if entry.file_type().is_symlink() {
return Err(BundleError::DirectorySymlink(entry.path().to_path_buf()));
}
if !entry.file_type().is_file() {
continue;
}
let relative = entry
.path()
.strip_prefix(root)
.map_err(|_| BundleError::UnexpectedDiskFile(entry.path().to_path_buf()))?;
if matches!(relative.to_str(), Some(MANIFEST | SECRETS)) {
continue;
}
let relative = BundleRelativePath::from_str(
relative
.to_str()
.ok_or_else(|| BundleError::UnexpectedDiskFile(relative.to_path_buf()))?,
)?;
if !manifest.files.contains_key(&relative) {
return Err(BundleError::UnexpectedDiskFile(entry.path().to_path_buf()));
}
}
Ok(())
}
fn usable_parent(path: &Path) -> &Path {
path.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
}
fn io_error(path: &Path, source: std::io::Error) -> BundleError {
BundleError::Io {
path: path.to_path_buf(),
source,
}
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) -> Result<(), BundleError> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(mode & 0o777))
.map_err(|source| io_error(path, source))
}
#[cfg(not(unix))]
fn set_mode(_path: &Path, _mode: u32) -> Result<(), BundleError> {
Ok(())
}
#[cfg(unix)]
fn verify_mode(path: &Path, expected: u32) -> Result<(), BundleError> {
use std::os::unix::fs::PermissionsExt;
let actual = fs::metadata(path)
.map_err(|source| io_error(path, source))?
.permissions()
.mode()
& 0o777;
if actual != expected & 0o777 {
return Err(BundleError::ModeMismatch(path.to_path_buf()));
}
Ok(())
}
#[cfg(not(unix))]
fn verify_mode(_path: &Path, _expected: u32) -> Result<(), BundleError> {
Ok(())
}