lib.rs

#![forbid(unsafe_code)]

//! FlashOS platform adapter for Flash.
//!
//! The target's existing Rust and `relibc` routes stay inside the concrete
//! adapter. Portable runtime code depends only on [`flash_platform::Platform`]
//! and never learns the Unix-like implementation details used below.

use std::ffi::{OsStr, OsString};
use std::path::{Component, Path, PathBuf};
use std::time::Duration;

use flash_platform::{
    Capabilities, Capability, ChildProcess, DescriptorEndpoint, DescriptorReadError,
    DirectoryReadError, DirectoryReadRequest, DirectoryStream, FileActionError, FileIoEndpoint,
    FileOpenRequest, ForegroundTerminalGuard, JobControlSignalGuard, JobSignal, PipeEndpoints,
    PipeError, Platform, PlatformError, ProcessGroupId, SignalError, SpawnError, SpawnRequest,
    StandardDirectories, StandardDirectoryEnvironment, TerminalModeGuard, TerminalModeToken,
    TerminalSize, WorkingDirectoryError, WorkingDirectoryRequest,
};
use flash_platform_posix::PosixPlatform;

/// The capabilities whose complete target behavior has been qualified.
///
/// Target runtime bring-up qualifies every classified group except signals.
///
/// FlashOS has not produced a stopped-child transition through the configured
/// target wait route, so the indivisible signals group remains absent rather
/// than advertising group delivery without its required transition vocabulary.
/// The versioned capability report records this bounded advertised set. The
/// exhaustive target matrix remains a separate gate. New groups likewise
/// default to absent until their complete behavior is explicitly brought up,
/// reported, and added here.
const QUALIFIED_CAPABILITIES: Capabilities = Capabilities::full_without(Capability::Signals);

/// The concrete FlashOS platform adapter.
#[derive(Clone, Copy, Debug)]
pub struct FlashOsPlatform {
    capabilities: Capabilities,
}

impl Default for FlashOsPlatform {
    fn default() -> Self {
        Self::new()
    }
}

impl FlashOsPlatform {
    /// Build the current adapter with only target-qualified capabilities.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            capabilities: QUALIFIED_CAPABILITIES,
        }
    }

    #[cfg(test)]
    const fn with_capabilities(capabilities: Capabilities) -> Self {
        Self { capabilities }
    }
}

/// FlashOS-owned selection of per-user directory roots.
#[derive(Clone, Copy, Debug, Default)]
pub struct FlashOsDirectoryPolicy;

impl FlashOsDirectoryPolicy {
    /// Select native home, configuration, cache, and state paths.
    ///
    /// Absolute `HOME` and XDG roots are preserved byte-for-byte. A missing or
    /// relative home falls back to `/root` for the root user, `/home/<user>` for
    /// a single native user-name component, and `/home/user` otherwise.
    #[must_use]
    pub fn select(environment: &dyn StandardDirectoryEnvironment) -> StandardDirectories {
        let home = absolute_environment_path(environment, "HOME")
            .unwrap_or_else(|| fallback_home(environment));
        let config = absolute_environment_path(environment, "XDG_CONFIG_HOME")
            .unwrap_or_else(|| home.join(".config"));
        let cache = absolute_environment_path(environment, "XDG_CACHE_HOME")
            .unwrap_or_else(|| home.join(".cache"));
        let state = absolute_environment_path(environment, "XDG_STATE_HOME")
            .unwrap_or_else(|| home.join(".local/state"));
        StandardDirectories::new(home, config, cache, state)
    }
}

impl Platform for FlashOsPlatform {
    fn capabilities(&self) -> Capabilities {
        self.capabilities
    }

    fn is_terminal(&self) -> bool {
        self.capabilities.supports(Capability::TerminalInfo) && PosixPlatform.is_terminal()
    }

    fn is_output_terminal(&self) -> bool {
        self.capabilities.supports(Capability::TerminalInfo) && PosixPlatform.is_output_terminal()
    }

    fn terminal_size(&self) -> Result<TerminalSize, PlatformError> {
        self.require(Capability::TerminalInfo)?;
        PosixPlatform.terminal_size()
    }

    fn enter_raw_mode(&self) -> Result<Box<dyn TerminalModeGuard>, PlatformError> {
        self.require(Capability::TerminalInfo)?;
        PosixPlatform.enter_raw_mode()
    }

    fn read_terminal_input(
        &self,
        buffer: &mut [u8],
        timeout: Duration,
    ) -> Result<Option<usize>, PlatformError> {
        self.require(Capability::TerminalInfo)?;
        PosixPlatform.read_terminal_input(buffer, timeout)
    }

    fn snapshot_terminal_mode(&self) -> Result<Box<dyn TerminalModeToken>, PlatformError> {
        self.require(Capability::TerminalInfo)?;
        PosixPlatform.snapshot_terminal_mode()
    }

    fn apply_terminal_mode(&self, token: &dyn TerminalModeToken) -> Result<(), PlatformError> {
        self.require(Capability::TerminalInfo)?;
        PosixPlatform.apply_terminal_mode(token)
    }

    fn foreground_process_group(&self) -> Result<Option<ProcessGroupId>, PlatformError> {
        self.require(Capability::ForegroundTerminal)?;
        PosixPlatform.foreground_process_group()
    }

    fn enter_foreground(
        &self,
        group: ProcessGroupId,
    ) -> Result<Box<dyn ForegroundTerminalGuard>, PlatformError> {
        self.require(Capability::ForegroundTerminal)?;
        PosixPlatform.enter_foreground(group)
    }

    fn signal_process_group(
        &self,
        group: ProcessGroupId,
        signal: JobSignal,
    ) -> Result<(), SignalError> {
        self.require(Capability::Signals)?;
        PosixPlatform.signal_process_group(group, signal)
    }

    fn install_job_control_signals(&self) -> Result<Box<dyn JobControlSignalGuard>, PlatformError> {
        self.require(Capability::Signals)?;
        PosixPlatform.install_job_control_signals()
    }

    fn shell_executable(&self) -> Result<PathBuf, PlatformError> {
        self.require(Capability::ShellExecutable)?;
        PosixPlatform.shell_executable()
    }

    fn ignore_hangup(&self) -> Result<(), PlatformError> {
        self.require(Capability::HangupDisposition)?;
        PosixPlatform.ignore_hangup()
    }

    fn standard_directories(
        &self,
        environment: &dyn StandardDirectoryEnvironment,
    ) -> Result<StandardDirectories, PlatformError> {
        self.require(Capability::StandardDirectories)?;
        Ok(FlashOsDirectoryPolicy::select(environment))
    }

    fn resolve_working_directory(
        &self,
        request: WorkingDirectoryRequest<'_>,
    ) -> Result<PathBuf, WorkingDirectoryError> {
        self.require(Capability::WorkingDirectory)?;
        PosixPlatform.resolve_working_directory(request)
    }

    fn pipe(&self) -> Result<PipeEndpoints, PipeError> {
        self.require(Capability::Pipes)?;
        PosixPlatform.pipe()
    }

    fn open_file(
        &self,
        request: FileOpenRequest<'_>,
    ) -> Result<Box<dyn DescriptorEndpoint>, FileActionError> {
        self.require(Capability::FileActions)?;
        PosixPlatform.open_file(request)
    }

    fn open_file_io(
        &self,
        request: FileOpenRequest<'_>,
    ) -> Result<Box<dyn FileIoEndpoint>, FileActionError> {
        self.require(Capability::FileActions)?;
        PosixPlatform.open_file_io(request)
    }

    fn inherit_descriptor(
        &self,
        descriptor: u32,
    ) -> Result<Box<dyn DescriptorEndpoint>, FileActionError> {
        self.require(Capability::FileActions)?;
        PosixPlatform.inherit_descriptor(descriptor)
    }

    fn read_directory(
        &self,
        request: DirectoryReadRequest<'_>,
    ) -> Result<Box<dyn DirectoryStream>, DirectoryReadError> {
        self.require(Capability::DirectoryRead)?;
        PosixPlatform.read_directory(request)
    }

    fn read_descriptor(
        &self,
        endpoint: &dyn DescriptorEndpoint,
        buffer: &mut [u8],
    ) -> Result<usize, DescriptorReadError> {
        self.require(Capability::Pipes)?;
        PosixPlatform.read_descriptor(endpoint, buffer)
    }

    fn spawn(&self, request: &SpawnRequest<'_>) -> Result<Box<dyn ChildProcess>, SpawnError> {
        self.require(Capability::ProcessSpawn)?;
        if request.process_group().requires_capability() {
            self.require(Capability::ProcessGroups)?;
        }
        PosixPlatform.spawn(request)
    }
}

fn absolute_environment_path(
    environment: &dyn StandardDirectoryEnvironment,
    name: &str,
) -> Option<PathBuf> {
    environment
        .value(OsStr::new(name))
        .filter(|value| !value.is_empty() && Path::new(value).is_absolute())
        .map(PathBuf::from)
}

fn fallback_home(environment: &dyn StandardDirectoryEnvironment) -> PathBuf {
    let Some(user) = environment
        .value(OsStr::new("USER"))
        .filter(is_single_normal_component)
    else {
        return PathBuf::from("/home/user");
    };
    if user == OsStr::new("root") {
        PathBuf::from("/root")
    } else {
        Path::new("/home").join(user)
    }
}

fn is_single_normal_component(value: &OsString) -> bool {
    let mut components = Path::new(value).components();
    matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none()
}

#[cfg(test)]
mod tests {
    use super::*;

    struct EmptyEnvironment;

    impl StandardDirectoryEnvironment for EmptyEnvironment {
        fn value(&self, _name: &OsStr) -> Option<OsString> {
            None
        }
    }

    #[test]
    fn a_qualified_directory_route_uses_flashos_policy() {
        let platform = FlashOsPlatform::with_capabilities(
            Capabilities::empty().with(Capability::StandardDirectories),
        );

        let directories = platform
            .standard_directories(&EmptyEnvironment)
            .expect("the qualified route selects directories");

        assert_eq!(directories.home(), Path::new("/home/user"));
    }
}

Return to documentation →