#![allow(unsafe_code)]
use crate::error::{ComposeError, Result};
use std::path::PathBuf;
#[cfg(unix)]
use std::path::Path;
pub fn is_safe_project_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 128
&& !name.starts_with('.')
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
}
#[cfg(unix)]
pub(super) fn staging_base() -> Result<PathBuf> {
let euid = unsafe { libc::geteuid() };
let base = match std::env::var_os("XDG_RUNTIME_DIR") {
Some(dir) if Path::new(&dir).is_absolute() => PathBuf::from(dir).join("podup"),
_ => std::env::temp_dir().join(format!("podup-{euid}")),
};
ensure_private_dir(&base, euid)?;
Ok(base)
}
#[cfg(windows)]
pub(super) fn staging_base() -> Result<PathBuf> {
let base = std::env::temp_dir().join("podup");
std::fs::create_dir_all(&base).map_err(ComposeError::Io)?;
let meta = std::fs::symlink_metadata(&base).map_err(ComposeError::Io)?;
if !meta.is_dir() || meta.file_type().is_symlink() {
return Err(ComposeError::Unsupported(format!(
"staging directory {} is not a private directory owned by the \
current user — refusing to use it",
base.display()
)));
}
Ok(base)
}
pub(super) fn reject_dangerous_secret_mode(mode: u32, ctx: &str) -> Result<()> {
if mode & 0o111 != 0 {
return Err(ComposeError::Unsupported(format!(
"mode {mode:#o} for {ctx} sets an execute bit on a secret/config; \
a secret holds data, never code (use e.g. 0o400 or 0o444)"
)));
}
if mode & (0o4000 | 0o2000 | 0o1000) != 0 {
return Err(ComposeError::Unsupported(format!(
"mode {mode:#o} for {ctx} sets setuid, setgid, or sticky bits on a \
secret/config; these are refused (use e.g. 0o400 or 0o444)"
)));
}
Ok(())
}
#[cfg(unix)]
fn ensure_private_dir(dir: &Path, euid: u32) -> Result<()> {
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(dir)
.map_err(ComposeError::Io)?;
let meta = std::fs::symlink_metadata(dir).map_err(ComposeError::Io)?;
if meta.is_dir() {
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
.map_err(ComposeError::Io)?;
}
verify_private_dir(dir, euid)
}
#[cfg(unix)]
fn verify_private_dir(dir: &Path, euid: u32) -> Result<()> {
use std::os::unix::fs::MetadataExt;
let meta = std::fs::symlink_metadata(dir).map_err(ComposeError::Io)?;
if !meta.is_dir() || meta.uid() != euid || meta.mode() & 0o077 != 0 {
return Err(ComposeError::Unsupported(format!(
"staging directory {} is not a private directory owned by the \
current user — refusing to use it",
dir.display()
)));
}
Ok(())
}
#[cfg(test)]
mod name_tests {
use super::is_safe_project_name;
#[test]
fn safe_project_names_accepted() {
for name in ["web", "my-app", "my_app", "app.v2", "A1"] {
assert!(is_safe_project_name(name), "{name:?} must be accepted");
}
}
#[test]
fn unsafe_project_names_rejected() {
let long = "a".repeat(129);
for name in [
"",
".",
"..",
".hidden",
"a/b",
"../x",
"a b",
"a\0b",
long.as_str(),
] {
assert!(!is_safe_project_name(name), "{name:?} must be rejected");
}
}
}
#[cfg(all(test, unix))]
mod staging_tests {
use super::verify_private_dir;
use std::os::unix::fs::PermissionsExt;
#[test]
fn private_dir_accepted() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700))
.expect("chmod");
let euid = unsafe { libc::geteuid() };
assert!(verify_private_dir(dir.path(), euid).is_ok());
}
#[test]
fn group_accessible_dir_rejected() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o750))
.expect("chmod");
let euid = unsafe { libc::geteuid() };
assert!(verify_private_dir(dir.path(), euid).is_err());
}
#[test]
fn foreign_owner_rejected() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700))
.expect("chmod");
let other = unsafe { libc::geteuid() } + 1;
assert!(verify_private_dir(dir.path(), other).is_err());
}
#[test]
fn symlink_rejected() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("real");
let link = dir.path().join("link");
std::fs::create_dir(&target).expect("mkdir");
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o700)).expect("chmod");
std::os::unix::fs::symlink(&target, &link).expect("symlink");
let euid = unsafe { libc::geteuid() };
assert!(verify_private_dir(&link, euid).is_err());
}
#[test]
fn regular_file_rejected() {
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join("file");
std::fs::write(&file, b"x").expect("write");
let euid = unsafe { libc::geteuid() };
assert!(verify_private_dir(&file, euid).is_err());
}
}
#[cfg(all(test, unix))]
mod ensure_dir_tests {
use super::ensure_private_dir;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
#[test]
fn creates_fresh_private_dir() {
let root = tempfile::tempdir().expect("tempdir");
let dir = root.path().join("base");
let euid = unsafe { libc::geteuid() };
ensure_private_dir(&dir, euid).expect("fresh dir");
let meta = std::fs::metadata(&dir).expect("metadata");
assert_eq!(meta.mode() & 0o777, 0o700);
}
#[test]
fn heals_drifted_permissions_on_owned_dir() {
let root = tempfile::tempdir().expect("tempdir");
let dir = root.path().join("base");
std::fs::create_dir(&dir).expect("mkdir");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).expect("chmod");
let euid = unsafe { libc::geteuid() };
ensure_private_dir(&dir, euid).expect("healed dir");
let meta = std::fs::metadata(&dir).expect("metadata");
assert_eq!(meta.mode() & 0o777, 0o700);
}
#[test]
fn symlinked_dir_is_rejected_not_healed() {
let root = tempfile::tempdir().expect("tempdir");
let target = root.path().join("real");
let link = root.path().join("link");
std::fs::create_dir(&target).expect("mkdir");
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o755)).expect("chmod");
std::os::unix::fs::symlink(&target, &link).expect("symlink");
let euid = unsafe { libc::geteuid() };
assert!(ensure_private_dir(&link, euid).is_err());
let meta = std::fs::metadata(&target).expect("metadata");
assert_eq!(meta.mode() & 0o777, 0o755);
}
}
#[cfg(test)]
mod reject_mode_tests {
use super::reject_dangerous_secret_mode;
#[test]
fn data_modes_accepted() {
assert!(reject_dangerous_secret_mode(0o400, "s").is_ok());
assert!(reject_dangerous_secret_mode(0o600, "s").is_ok());
assert!(reject_dangerous_secret_mode(0o444, "s").is_ok());
}
#[test]
fn execute_setuid_setgid_sticky_rejected() {
for mode in [0o100, 0o500, 0o700, 0o4000, 0o2000, 0o1000] {
assert!(
reject_dangerous_secret_mode(mode, "s").is_err(),
"{mode:#o} must be rejected"
);
}
}
}
#[cfg(all(test, windows))]
mod windows_staging_tests {
use super::staging_base;
#[test]
fn staging_base_is_a_directory() {
let base = staging_base().expect("staging base");
assert!(base.is_dir());
assert!(base.ends_with("podup"));
}
}