use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::model::manifest::ManifestDocument;
use crate::bundle::{BundleError, BundlePath, MANIFEST_FILE};
const EXECUTABLE_MODE: u32 = 0o755;
const DATA_MODE: u32 = 0o644;
#[derive(Clone, Debug)]
pub(crate) struct BundleRoot {
path: PathBuf,
}
impl BundleRoot {
pub(crate) fn open(requested: &Path) -> Result<Self, BundleError> {
let metadata = std::fs::metadata(requested).map_err(|source| BundleError::Root {
path: requested.to_path_buf(),
source,
})?;
if !metadata.is_dir() {
return Err(BundleError::NotDirectory(requested.to_path_buf()));
}
Ok(Self {
path: requested.to_path_buf(),
})
}
pub(crate) fn path(&self) -> &Path {
&self.path
}
pub(crate) fn relocate(&mut self, path: PathBuf) {
self.path = path;
}
}
pub(crate) fn open_bundle_file(
root: &BundleRoot,
path: &BundlePath,
) -> Result<std::fs::File, BundleError> {
let filesystem_path = path.filesystem_path(root.path());
match std::fs::File::open(&filesystem_path) {
Ok(file) => Ok(file),
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
Err(BundleError::MissingFile {
path: filesystem_path,
})
}
Err(source) => Err(BundleError::ReadFile {
path: filesystem_path,
source,
}),
}
}
pub(crate) fn open_executable_source(path: &Path) -> Result<std::fs::File, BundleError> {
let file = std::fs::File::open(path).map_err(|source| BundleError::ReadFile {
path: path.to_path_buf(),
source,
})?;
let metadata = file.metadata().map_err(|source| BundleError::ReadFile {
path: path.to_path_buf(),
source,
})?;
if !metadata.is_file() {
return Err(BundleError::UnsupportedEntry {
path: path.to_path_buf(),
});
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if metadata.permissions().mode() & 0o111 == 0 {
return Err(BundleError::NotExecutable {
path: path.to_path_buf(),
});
}
}
Ok(file)
}
pub(crate) fn ensure_staging_directory(
root: &BundleRoot,
relative: &str,
) -> Result<(), BundleError> {
let mut path = root.path().to_path_buf();
for component in relative.split('/') {
path.push(component);
match std::fs::create_dir(&path) {
Ok(()) => set_mode(&path, EXECUTABLE_MODE)?,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(source) => return Err(BundleError::ReadFile { path, source }),
}
}
Ok(())
}
fn create_staging_file(
root: &BundleRoot,
path: &BundlePath,
mode: u32,
) -> Result<std::fs::File, BundleError> {
if let Some((parent, _)) = path.as_str().rsplit_once('/') {
ensure_staging_directory(root, parent)?;
}
let filesystem_path = path.filesystem_path(root.path());
let file =
std::fs::File::create_new(&filesystem_path).map_err(|source| BundleError::ReadFile {
path: filesystem_path.clone(),
source,
})?;
set_mode(&filesystem_path, mode)?;
Ok(file)
}
pub(crate) fn publish_staging_root(staged: &Path, target: &Path) -> Result<(), BundleError> {
std::fs::rename(staged, target).map_err(|source| BundleError::ReadFile {
path: target.to_path_buf(),
source,
})
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) -> Result<(), BundleError> {
std::fs::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(mode)).map_err(
|source| BundleError::ReadFile {
path: path.to_path_buf(),
source,
},
)
}
#[cfg(not(unix))]
fn set_mode(_path: &Path, _mode: u32) -> Result<(), BundleError> {
Ok(())
}
pub(crate) fn read_manifest_document(root: &BundleRoot) -> Result<ManifestDocument, BundleError> {
let manifest_path = root.path().join(MANIFEST_FILE);
let mut file =
open_bundle_file(root, &BundlePath::new(MANIFEST_FILE)?).map_err(|error| match error {
BundleError::ReadFile { path, source } => BundleError::ReadManifest { path, source },
BundleError::MissingFile { path } => BundleError::ReadManifest {
path,
source: std::io::Error::new(std::io::ErrorKind::NotFound, MANIFEST_FILE),
},
other => other,
})?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|source| BundleError::ReadManifest {
path: manifest_path,
source,
})?;
Ok(serde_json::from_slice(&bytes)?)
}
pub(crate) fn write_new_file(
root: &BundleRoot,
path: &BundlePath,
bytes: &[u8],
) -> Result<(), BundleError> {
let mut file = create_staging_file(root, path, DATA_MODE)?;
let diagnostic = path.filesystem_path(root.path());
std::io::Write::write_all(&mut file, bytes).map_err(|source| BundleError::ReadFile {
path: diagnostic,
source,
})
}
pub(crate) fn copy_executable_source(
root: &BundleRoot,
source: &Path,
destination: &BundlePath,
) -> Result<(), BundleError> {
let mut input = open_executable_source(source)?;
let mut output = create_staging_file(root, destination, EXECUTABLE_MODE)?;
let diagnostic = destination.filesystem_path(root.path());
std::io::copy(&mut input, &mut output).map_err(|error| BundleError::ReadFile {
path: diagnostic,
source: error,
})?;
Ok(())
}
pub(crate) fn prepare_publish_parent(root: &Path) -> Result<PathBuf, BundleError> {
let parent = root.parent().unwrap_or_else(|| Path::new("."));
let canonical_parent = parent
.canonicalize()
.map_err(|source| BundleError::ReadFile {
path: parent.to_path_buf(),
source,
})?;
let metadata =
std::fs::symlink_metadata(&canonical_parent).map_err(|source| BundleError::ReadFile {
path: canonical_parent.clone(),
source,
})?;
if !metadata.is_dir() {
return Err(BundleError::NotDirectory(canonical_parent));
}
let name = root.file_name().ok_or_else(|| BundleError::ReadFile {
path: root.to_path_buf(),
source: std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid bundle name"),
})?;
Ok(canonical_parent.join(name))
}
pub(crate) fn reject_existing_target(root: &Path) -> Result<(), BundleError> {
match std::fs::symlink_metadata(root) {
Ok(_) => Err(BundleError::TargetExists(root.to_path_buf())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(BundleError::ReadFile {
path: root.to_path_buf(),
source,
}),
}
}
pub(crate) fn create_staging_root(target: &Path) -> Result<PathBuf, BundleError> {
let parent = target.parent().unwrap_or_else(|| Path::new("."));
let name = target
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| BundleError::ReadFile {
path: target.to_path_buf(),
source: std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid bundle name"),
})?;
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_nanos());
for attempt in 0..100u32 {
let staged = parent.join(format!(
".{name}.staging-{}-{stamp}-{attempt}",
std::process::id()
));
match std::fs::create_dir(&staged) {
Ok(()) => {
set_mode(&staged, EXECUTABLE_MODE)?;
return Ok(staged);
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(source) => {
return Err(BundleError::ReadFile {
path: staged,
source,
});
}
}
}
Err(BundleError::ReadFile {
path: parent.to_path_buf(),
source: std::io::Error::new(std::io::ErrorKind::AlreadyExists, "staging name exhausted"),
})
}