use std::fs::{self, File as FsFile, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use semver::{Version, VersionReq};
use crate::model::{Error, Result, validate_name};
const LOCKS_DIRECTORY: &str = ".locks";
static UNIQUE_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) struct Publication {
pub(crate) version: Version,
pub(crate) executable: PathBuf,
}
pub(crate) fn publish(root: &Path, name: &str, version: &str, executable: &[u8]) -> Result<()> {
validate_name(name)?;
let version = stable_version(version)?;
if executable.is_empty() {
return Err(Error::new("invalid_publication", "the executable is empty"));
}
let root = checked_root(root, true)?;
let locks = root.join(LOCKS_DIRECTORY);
create_checked_directory(&locks, "publication lock root")?;
let name_locks = locks.join(name);
create_checked_directory(&name_locks, "binary publication lock directory")?;
let _lock = lock_file(&name_locks.join(".lock"))?;
let binary_root = root.join(name);
create_checked_directory(&binary_root, "published binary directory")?;
let destination = binary_root.join(format!("v{version}"));
if path_exists(&destination)? {
return Err(Error::new(
"already_published",
format!("binary {name:?} version {version} is already published"),
));
}
let staging = binary_root.join(format!(".staging-{}", unique_id()));
fs::create_dir(&staging)
.map_err(|error| Error::io("create staged binary publication", &staging, error))?;
let result = (|| {
let executable_path = staging.join(name);
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&executable_path)
.map_err(|error| {
Error::io(
"create staged published executable",
&executable_path,
error,
)
})?;
file.write_all(executable).map_err(|error| {
Error::io("write staged published executable", &executable_path, error)
})?;
file.sync_all().map_err(|error| {
Error::io("sync staged published executable", &executable_path, error)
})?;
set_executable(&executable_path)?;
sync_directory(&staging)?;
if path_exists(&destination)? {
return Err(Error::new(
"already_published",
format!("binary {name:?} version {version} is already published"),
));
}
fs::rename(&staging, &destination)
.map_err(|error| Error::io("install binary publication", &destination, error))?;
sync_directory(&binary_root)
})();
if result.is_err() {
let _ = fs::remove_dir_all(&staging);
}
result
}
pub(crate) fn resolve(root: &Path, name: &str, selector: &str) -> Result<Publication> {
validate_name(name)?;
if selector.is_empty() || selector.len() > 512 || selector.contains(['/', '\0']) {
return Err(Error::new(
"invalid_selector",
"invalid binary version selector",
));
}
let exact = selector
.strip_prefix('v')
.and_then(|value| Version::parse(value).ok())
.filter(|version| {
version.to_string() == selector.trim_start_matches('v')
&& version.pre.is_empty()
&& version.build.is_empty()
});
let requirement = if exact.is_none() {
let value = selector.strip_prefix('v').unwrap_or(selector);
Some(VersionReq::parse(value).map_err(|error| {
Error::new(
"invalid_selector",
format!("invalid Cargo-compatible version requirement: {error}"),
)
})?)
} else {
None
};
let root = checked_root(root, false)?;
let binary_root = checked_directory(&root.join(name), "published binary")?;
let entries = fs::read_dir(&binary_root)
.map_err(|error| Error::io("list published binary versions", &binary_root, error))?;
let mut selected = None;
for entry in entries {
let entry =
entry.map_err(|error| Error::io("read published binary entry", &binary_root, error))?;
let path = entry.path();
let metadata = fs::symlink_metadata(&path)
.map_err(|error| Error::io("inspect published binary version", &path, error))?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
continue;
}
let Some(directory) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
let Some(version_text) = directory.strip_prefix('v') else {
continue;
};
let Ok(version) = Version::parse(version_text) else {
continue;
};
if version.to_string() != version_text
|| !version.pre.is_empty()
|| !version.build.is_empty()
{
continue;
}
let matches = exact.as_ref().is_some_and(|exact| exact == &version)
|| requirement
.as_ref()
.is_some_and(|requirement| requirement.matches(&version));
if matches && selected.as_ref().is_none_or(|current| &version > current) {
selected = Some(version);
}
}
let version = selected.ok_or_else(|| {
Error::new(
"not_found",
format!("no published binary {name:?} matches {selector:?}"),
)
})?;
let version_root = checked_directory(
&binary_root.join(format!("v{version}")),
"published binary version",
)?;
let executable = version_root.join(name);
let metadata = fs::symlink_metadata(&executable)
.map_err(|error| Error::io("inspect published executable", &executable, error))?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(Error::new(
"invalid_publication",
format!(
"published executable is not a regular file: {}",
executable.display()
),
));
}
Ok(Publication {
version,
executable,
})
}
fn stable_version(value: &str) -> Result<Version> {
let version = Version::parse(value).map_err(|error| {
Error::new(
"invalid_publication",
format!("invalid binary version {value:?}: {error}"),
)
})?;
if version.to_string() != value || !version.pre.is_empty() || !version.build.is_empty() {
return Err(Error::new(
"invalid_publication",
format!("binary version is not canonical stable SemVer: {value:?}"),
));
}
Ok(version)
}
fn checked_root(path: &Path, create: bool) -> Result<PathBuf> {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map_err(|error| Error::io("read current directory", ".", error))?
.join(path)
};
if create {
fs::create_dir_all(&absolute)
.map_err(|error| Error::io("create binary publication root", &absolute, error))?;
}
checked_directory(&absolute, "binary publication root")?;
fs::canonicalize(&absolute)
.map_err(|error| Error::io("canonicalize binary publication root", &absolute, error))
}
fn create_checked_directory(path: &Path, label: &str) -> Result<()> {
fs::create_dir_all(path).map_err(|error| Error::io("create directory", path, error))?;
checked_directory(path, label).map(|_| ())
}
fn checked_directory(path: &Path, label: &str) -> Result<PathBuf> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => Err(Error::new(
"invalid_publication",
format!("{label} is not a regular directory: {}", path.display()),
)),
Ok(_) => Ok(path.to_path_buf()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Err(Error::new(
"not_found",
format!("{label} does not exist: {}", path.display()),
)),
Err(error) => Err(Error::io("inspect directory", path, error)),
}
}
fn path_exists(path: &Path) -> Result<bool> {
match fs::symlink_metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(Error::io("inspect publication path", path, error)),
}
}
fn lock_file(path: &Path) -> Result<PublicationLock> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
return Err(Error::new(
"invalid_publication",
format!("publication lock is not a regular file: {}", path.display()),
));
}
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(Error::io("inspect publication lock", path, error)),
}
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(path)
.map_err(|error| Error::io("open publication lock", path, error))?;
if !file
.metadata()
.map_err(|error| Error::io("inspect opened publication lock", path, error))?
.is_file()
{
return Err(Error::new(
"invalid_publication",
"opened publication lock is not a regular file",
));
}
FsFile::lock(&file).map_err(|error| Error::io("lock binary publication", path, error))?;
Ok(PublicationLock(file))
}
struct PublicationLock(FsFile);
impl Drop for PublicationLock {
fn drop(&mut self) {
let _ = FsFile::unlock(&self.0);
}
}
fn sync_directory(path: &Path) -> Result<()> {
FsFile::open(path)
.and_then(|directory| directory.sync_all())
.map_err(|error| Error::io("sync directory", path, error))
}
#[cfg(unix)]
fn set_executable(path: &Path) -> Result<()> {
let mut permissions = fs::metadata(path)
.map_err(|error| Error::io("inspect published executable", path, error))?
.permissions();
permissions.set_mode(0o500);
fs::set_permissions(path, permissions)
.map_err(|error| Error::io("mark published executable", path, error))
}
#[cfg(not(unix))]
fn set_executable(_path: &Path) -> Result<()> {
Err(Error::new(
"unsupported_host",
"published executables require a Unix host",
))
}
fn unique_id() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let counter = UNIQUE_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{:x}-{nanos:x}-{counter:x}", std::process::id())
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use super::{publish, resolve};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TestRoot(PathBuf);
impl TestRoot {
fn new() -> Self {
let counter = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"kcode-rust-bin-publications-{}-{counter}",
std::process::id()
));
fs::create_dir(&path).unwrap();
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TestRoot {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn publication_is_immutable_and_cargo_requirements_select_the_highest_match() {
let root = TestRoot::new();
publish(root.path(), "demo", "1.2.3", b"one").unwrap();
publish(root.path(), "demo", "1.9.0", b"two").unwrap();
publish(root.path(), "demo", "2.0.0", b"three").unwrap();
let exact = resolve(root.path(), "demo", "v1.2.3").unwrap();
assert_eq!(exact.version.to_string(), "1.2.3");
assert_eq!(fs::read(exact.executable).unwrap(), b"one");
let compatible = resolve(root.path(), "demo", "1.2").unwrap();
assert_eq!(compatible.version.to_string(), "1.9.0");
assert_eq!(fs::read(compatible.executable).unwrap(), b"two");
let duplicate = publish(root.path(), "demo", "1.2.3", b"different").unwrap_err();
assert!(duplicate.to_string().starts_with("already_published:"));
}
}