use super::{InclusionReason, PLAN_ENTRIES_MAX, PlannedFile, PlannedFileKind, mode_of};
use crate::{
error::{Error, Result, io},
hash::{DigestCache, sha256_bytes},
paths::ancestor_dirs,
source::{EntryKind, SourceRoot},
};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
#[derive(Debug)]
pub(super) struct PlanBuilder<'a> {
root: &'a SourceRoot,
entries: BTreeMap<PathBuf, PlannedFile>,
digests: DigestCache,
}
impl<'a> PlanBuilder<'a> {
pub(super) fn new(root: &'a SourceRoot) -> PlanBuilder<'a> {
PlanBuilder {
root,
entries: BTreeMap::new(),
digests: DigestCache::new(),
}
}
fn insert(&mut self, file: PlannedFile) {
file.assert_well_formed();
match self.entries.get(&file.destination) {
Some(existing) if existing.kind != PlannedFileKind::Directory => {}
Some(_) if file.kind == PlannedFileKind::Directory => {}
_ => {
self.entries.insert(file.destination.clone(), file);
}
}
}
fn push_parents(&mut self, path: &Path, reason: &InclusionReason) {
assert!(path.is_absolute());
for dir in ancestor_dirs(path) {
self.insert(PlannedFile {
source: None,
destination: dir,
kind: PlannedFileKind::Directory,
reason: reason.clone(),
mode: 0o755,
size: 0,
sha256: None,
link_target: None,
content: None,
});
}
}
pub(super) fn push_file(&mut self, file: PlannedFile) {
self.push_parents(&file.destination, &file.reason);
self.insert(file);
}
pub(super) fn push_symlink(&mut self, logical: &Path, target: &Path, reason: InclusionReason) {
assert!(logical.is_absolute());
assert!(!target.as_os_str().is_empty());
self.push_parents(logical, &reason);
self.insert(PlannedFile {
source: None,
destination: logical.to_path_buf(),
kind: PlannedFileKind::Symlink,
reason,
mode: 0o777,
size: 0,
sha256: None,
link_target: Some(target.to_path_buf()),
content: None,
});
}
pub(super) fn push_dir_with_mode(&mut self, path: &Path, mode: u32, reason: InclusionReason) {
self.push_parents(path, &reason);
self.insert(PlannedFile {
source: None,
destination: path.to_path_buf(),
kind: PlannedFileKind::Directory,
reason,
mode,
size: 0,
sha256: None,
link_target: None,
content: None,
});
}
pub(super) fn push_generated(
&mut self,
path: &Path,
content: Vec<u8>,
reason: InclusionReason,
) {
assert!(path.is_absolute());
assert!(!content.is_empty());
self.push_parents(path, &reason);
let digest = sha256_bytes(&content);
self.insert(PlannedFile {
source: None,
destination: path.to_path_buf(),
kind: PlannedFileKind::RuntimeConfig,
reason,
mode: 0o644,
size: content.len() as u64,
sha256: Some(digest),
link_target: None,
content: Some(content),
});
}
fn push_source_file(
&mut self,
source: PathBuf,
destination: PathBuf,
kind: PlannedFileKind,
reason: InclusionReason,
) -> Result<()> {
assert!(destination.is_absolute());
let (digest, size) = self.digests.get(&source)?;
self.push_file(PlannedFile {
source: Some(source.clone()),
destination,
kind,
reason,
mode: mode_of(&source)?,
size,
sha256: Some(digest),
link_target: None,
content: None,
});
Ok(())
}
pub(super) fn copy_path(
&mut self,
logical: &Path,
kind: PlannedFileKind,
reason: InclusionReason,
recursive: bool,
) -> Result<bool> {
let Some(resolved) = self.root.resolve(logical)? else {
return Ok(false);
};
for link in &resolved.links {
self.push_symlink(&link.logical, &link.target, reason.clone());
}
match resolved.kind {
EntryKind::File => {
self.push_source_file(resolved.host, resolved.logical, kind, reason)?;
Ok(true)
}
EntryKind::Directory if recursive => {
self.push_dir_with_mode(
&resolved.logical,
mode_of(&resolved.host)?,
reason.clone(),
);
self.copy_tree(&resolved.logical, &resolved.host, kind, &reason)?;
Ok(true)
}
EntryKind::Directory | EntryKind::Other => Ok(false),
}
}
fn copy_tree(
&mut self,
logical: &Path,
host: &Path,
kind: PlannedFileKind,
reason: &InclusionReason,
) -> Result<()> {
let mut stack = vec![(logical.to_path_buf(), host.to_path_buf())];
while let Some((logical, host)) = stack.pop() {
assert!(logical.is_absolute());
self.check_size(&logical)?;
let mut names = Vec::new();
let remaining = PLAN_ENTRIES_MAX.saturating_sub(self.entries.len());
for entry in std::fs::read_dir(&host).map_err(|e| io(&host, e))? {
if names.len() == remaining {
return Err(Self::size_error(&logical));
}
let entry = entry.map_err(|e| io(&host, e))?;
names.push(entry.file_name());
}
names.sort();
for name in names {
let child_logical = logical.join(&name);
self.check_size(&child_logical)?;
let child_host = host.join(&name);
let metadata =
std::fs::symlink_metadata(&child_host).map_err(|e| io(&child_host, e))?;
if metadata.is_symlink() {
let target = std::fs::read_link(&child_host).map_err(|e| io(&child_host, e))?;
self.push_symlink(&child_logical, &target, reason.clone());
} else if metadata.is_dir() {
self.push_dir_with_mode(&child_logical, mode_of(&child_host)?, reason.clone());
stack.push((child_logical, child_host));
} else if metadata.is_file() {
self.push_source_file(child_host, child_logical, kind, reason.clone())?;
}
self.check_size(&logical)?;
}
}
Ok(())
}
fn check_size(&self, logical: &Path) -> Result<()> {
if self.entries.len() <= PLAN_ENTRIES_MAX {
return Ok(());
}
Err(Self::size_error(logical))
}
fn size_error(logical: &Path) -> Error {
Error::Config {
message: format!(
"bundle plan exceeds {PLAN_ENTRIES_MAX} entries at `{}`; narrow --include",
logical.display()
),
}
}
pub(super) fn finish(self) -> Vec<PlannedFile> {
self.entries.into_values().collect()
}
}