use crate::pid_namespace::{ExpiredForeignRecord, current_pid_namespace, foreign_owner_standing};
use crate::process_identity::{current_birth_identity, process_birth_identity};
use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure_of};
use std::path::Path;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CopyOwner {
pub pid: i64,
pub birth_identity: String,
pub pid_namespace: Option<String>,
}
const FIELD_SEPARATOR: char = '~';
const ESCAPE: char = '%';
fn is_plain(byte: u8) -> bool {
return byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'-' | b'[' | b']');
}
fn escape_field(field: &str) -> String {
let mut written: String = String::with_capacity(field.len());
for byte in field.bytes() {
if is_plain(byte) {
written.push(char::from(byte));
} else {
written.push_str(format!("{ESCAPE}{byte:02x}").as_str());
}
}
return written;
}
fn unescape_field(text: &str) -> Option<String> {
let mut bytes: Vec<u8> = Vec::with_capacity(text.len());
let mut remaining = text.bytes();
while let Some(byte) = remaining.next() {
if char::from(byte) == ESCAPE {
let high: u8 = remaining.next()?;
let low: u8 = remaining.next()?;
let pair: [u8; 2] = [high, low];
bytes.push(u8::from_str_radix(std::str::from_utf8(&pair).ok()?, 16).ok()?);
} else {
bytes.push(byte);
}
}
return String::from_utf8(bytes).ok();
}
pub fn current_copy_owner() -> Result<CopyOwner, WorktreeCopyError> {
let birth: String = match current_birth_identity() {
Ok(found) => found,
Err(error) => return Err(failure_with_cause("monochromatic-git-policy-cli: the identity of this process is unavailable, so a copy cannot name its owner.", Some(&error))),
};
return Ok(CopyOwner {
pid: i64::from(std::process::id()),
birth_identity: birth,
pid_namespace: current_pid_namespace(),
});
}
impl CopyOwner {
pub fn token(&self) -> String {
let namespace: String = escape_field(self.pid_namespace.as_deref().unwrap_or(""));
return format!("{}{FIELD_SEPARATOR}{}{FIELD_SEPARATOR}{namespace}", self.pid, escape_field(self.birth_identity.as_str()));
}
pub fn from_token(token: &str) -> Option<CopyOwner> {
let mut fields = token.split(FIELD_SEPARATOR);
let pid: i64 = fields.next()?.parse::<i64>().ok()?;
if pid < 1 {
return None;
}
let birth: String = unescape_field(fields.next()?)?;
if birth.is_empty() {
return None;
}
let namespace: String = unescape_field(fields.next()?)?;
if fields.next().is_some() {
return None;
}
return Some(CopyOwner {
pid,
birth_identity: birth,
pid_namespace: if namespace.is_empty() { None } else { Some(namespace) },
});
}
pub fn expired_foreign_refusal(&self, item: &Path) -> Option<WorktreeCopyError> {
return match foreign_owner_standing(self.pid_namespace.as_deref(), item) {
Some(Err(expired)) => Some(WorktreeCopyError(expired.describe(self.pid))),
Some(Ok(())) | None => None,
};
}
pub fn is_alive(&self, item: &Path) -> Result<bool, WorktreeCopyError> {
return self.alive_by(foreign_owner_standing(self.pid_namespace.as_deref(), item));
}
fn alive_by(&self, standing: Option<Result<(), ExpiredForeignRecord>>) -> Result<bool, WorktreeCopyError> {
match standing {
Some(Err(expired)) => return Err(WorktreeCopyError(expired.describe(self.pid))),
Some(Ok(())) => return Ok(true),
None => {}
}
match process_birth_identity(self.pid).map_err(raw_failure_of)? {
Some(current) => return Ok(current == self.birth_identity),
None => return Ok(false),
}
}
}
pub fn token_owner_is_dead(token: Option<&str>, item: &Path) -> Result<bool, WorktreeCopyError> {
let Some(owner) = token.and_then(CopyOwner::from_token) else {
return Ok(true);
};
let standing = foreign_owner_standing(owner.pid_namespace.as_deref(), item);
if let Some(Err(expired)) = &standing {
return Err(WorktreeCopyError(expired.describe(owner.pid)));
}
match owner.alive_by(standing) {
Ok(alive) => return Ok(!alive),
Err(_unproven) => return Ok(false),
}
}
#[cfg(test)]
#[path = "worktree_copy_owner_tests.rs"]
mod tests;