regy 0.1.0

Private-by-default desktop agent for the Regy web interface
use std::{
    fs,
    io::{Seek, Write},
    os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt},
    path::Path,
};

use crate::domain::errors::{AgentError, AgentResult, ErrorCode};

pub(super) const PRIVATE_FILE_MODE: u32 = 0o600;

pub(crate) struct RuntimeLock {
    _file: fs::File,
}

impl std::fmt::Debug for RuntimeLock {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str("RuntimeLock { redacted }")
    }
}

impl RuntimeLock {
    pub(crate) fn acquire(path: &Path) -> AgentResult<Self> {
        let parent = path.parent().ok_or_else(lock_unavailable)?;
        fs::create_dir_all(parent).map_err(|_| lock_unavailable())?;
        let mut options = fs::OpenOptions::new();
        options
            .read(true)
            .write(true)
            .create(true)
            .mode(PRIVATE_FILE_MODE)
            .custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
        let mut file = options.open(path).map_err(|_| lock_unavailable())?;
        validate_lock_file(&file)?;
        match rustix::fs::flock(&file, rustix::fs::FlockOperation::NonBlockingLockExclusive) {
            Ok(()) => {
                file.set_len(0).map_err(|_| lock_unavailable())?;
                file.rewind().map_err(|_| lock_unavailable())?;
                writeln!(file, "{}", std::process::id()).map_err(|_| lock_unavailable())?;
                file.sync_data().map_err(|_| lock_unavailable())?;
                Ok(Self { _file: file })
            }
            Err(error)
                if error == rustix::io::Errno::WOULDBLOCK || error == rustix::io::Errno::AGAIN =>
            {
                Err(already_running())
            }
            Err(_) => Err(lock_unavailable()),
        }
    }
}

pub(super) fn open_existing_lock(path: &Path) -> AgentResult<Option<fs::File>> {
    let mut options = fs::OpenOptions::new();
    options
        .read(true)
        .write(true)
        .custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
    match options.open(path) {
        Ok(file) => {
            validate_lock_file(&file)?;
            Ok(Some(file))
        }
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(_) => Err(lock_unavailable()),
    }
}

pub(super) fn validate_lock_file(file: &fs::File) -> AgentResult<()> {
    let metadata = file.metadata().map_err(|_| lock_unavailable())?;
    if !metadata.is_file()
        || metadata.permissions().mode() & 0o777 != PRIVATE_FILE_MODE
        || metadata.nlink() != 1
        || metadata.uid() != rustix::process::getuid().as_raw()
    {
        return Err(lock_unavailable());
    }
    Ok(())
}

fn already_running() -> AgentError {
    AgentError::new(
        ErrorCode::InvalidMessage,
        "PC Agent server is already running for this data directory",
    )
}

pub(super) fn lock_unavailable() -> AgentError {
    AgentError::new(
        ErrorCode::InvalidMessage,
        "PC Agent server lock is unavailable",
    )
}