use crate::pid_namespace::{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};
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 = current_birth_identity().map_err(|error| {
return failure_with_cause("cli-git: 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().filter(|found| { return *found >= 1; })?;
let birth: String = unescape_field(fields.next()?).filter(|found| { return !found.is_empty(); })?;
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> {
if let Some(refusal) = self.expired_foreign_refusal(item) {
return Err(refusal);
}
if foreign_owner_standing(self.pid_namespace.as_deref(), item).is_some() {
return Ok(true);
}
match process_birth_identity(self.pid).map_err(|error| return raw_failure(&error))? {
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);
};
if let Some(refusal) = owner.expired_foreign_refusal(item) {
return Err(refusal);
}
match owner.is_alive(item) {
Ok(alive) => return Ok(!alive),
Err(_unproven) => return Ok(false),
}
}
#[cfg(test)]
#[path = "worktree_copy_owner_tests.rs"]
mod tests;