alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Platform-specific filesystem identity and no-follow handle helpers.

#[cfg(not(unix))]
use super::path::require_under_root;
use super::{config::FilesystemConfig, error::FileReadError};
use std::{
    fs::{File, Metadata},
    io,
    path::{Component, Path},
};

#[cfg(unix)]
use rustix::fs::{Mode, OFlags, openat};

/// Opens an existing policy-resolved file and returns metadata from the opened handle.
pub(super) fn open_existing_file(
    path: &Path,
    config: &FilesystemConfig,
) -> Result<(File, Metadata), FileReadError> {
    #[cfg(unix)]
    {
        open_existing_file_at_workspace_root(path, config).map_err(|source| FileReadError::Open {
            path: path.to_owned(),
            source,
        })
    }

    #[cfg(not(unix))]
    {
        let file = File::open(path).map_err(|source| FileReadError::Open {
            path: path.to_owned(),
            source,
        })?;
        let metadata = file.metadata().map_err(|source| FileReadError::Metadata {
            path: path.to_owned(),
            source,
        })?;
        require_current_path_identity(path, &metadata, config).map_err(|source| {
            FileReadError::Metadata {
                path: path.to_owned(),
                source,
            }
        })?;
        Ok((file, metadata))
    }
}

/// Verifies that the opened handle still corresponds to the policy-resolved path.
#[cfg(not(unix))]
pub(super) fn require_current_path_identity(
    path: &Path,
    opened: &Metadata,
    config: &FilesystemConfig,
) -> Result<(), io::Error> {
    let canonical = path.canonicalize()?;
    require_under_root(&canonical, config)
        .map_err(|_error| io::Error::other("opened file resolved outside workspace root"))?;

    let current = std::fs::symlink_metadata(path)?;
    if same_file_identity(opened, &current) {
        Ok(())
    } else {
        Err(io::Error::other("opened file changed during policy check"))
    }
}

/// Returns whether two metadata values describe the same file identity.
#[cfg(not(unix))]
fn same_file_identity(left: &Metadata, right: &Metadata) -> bool {
    let Some(left) = FileIdentity::from_metadata(left) else {
        return false;
    };
    let Some(right) = FileIdentity::from_metadata(right) else {
        return false;
    };

    left == right
}

/// Platform file identity for non-Unix policy revalidation.
#[cfg(not(unix))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct FileIdentity {
    /// Volume identity.
    volume: u64,
    /// File identity on the volume.
    file: u128,
}

#[cfg(not(unix))]
impl FileIdentity {
    /// Builds a file identity from metadata when the platform exposes stable IDs.
    fn from_metadata(metadata: &Metadata) -> Option<Self> {
        file_identity_from_metadata(metadata)
    }
}

/// Builds Windows file identity from volume and file-index metadata.
#[cfg(windows)]
fn file_identity_from_metadata(metadata: &Metadata) -> Option<FileIdentity> {
    use std::os::windows::fs::MetadataExt;

    Some(FileIdentity {
        volume: u64::from(metadata.volume_serial_number()?),
        file: (u128::from(metadata.file_index_high()) << 64)
            | u128::from(metadata.file_index_low()),
    })
}

/// Fails closed on non-Unix platforms without a stable file-identity API.
#[cfg(all(not(unix), not(windows)))]
fn file_identity_from_metadata(_metadata: &Metadata) -> Option<FileIdentity> {
    None
}

/// Opens a resolved path by walking from the workspace root through no-follow directory handles.
#[cfg(unix)]
fn open_existing_file_at_workspace_root(
    path: &Path,
    config: &FilesystemConfig,
) -> Result<(File, Metadata), io::Error> {
    let relative_path = path
        .strip_prefix(&config.workspace_root)
        .map_err(|_error| {
            io::Error::new(
                io::ErrorKind::PermissionDenied,
                "path is outside workspace root",
            )
        })?;
    if relative_path.as_os_str().is_empty() {
        let file = open_workspace_directory(Path::new(""), config)?;
        let metadata = file.metadata()?;
        return Ok((file, metadata));
    }
    let (parent, file_name) = split_parent_and_file_name(relative_path)?;
    let parent_dir = open_workspace_directory(parent, config)?;
    let file_fd = openat(
        &parent_dir,
        file_name,
        OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
        Mode::empty(),
    )?;
    let file = File::from(file_fd);
    let metadata = file.metadata()?;

    Ok((file, metadata))
}

/// Opens a workspace-contained directory by walking components without following symlinks.
#[cfg(unix)]
pub(super) fn open_workspace_directory(
    relative_dir: &Path,
    config: &FilesystemConfig,
) -> Result<File, io::Error> {
    let mut current = File::from(openat(
        rustix::fs::CWD,
        &config.workspace_root,
        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
        Mode::empty(),
    )?);

    for component in normal_components(relative_dir)? {
        let next = File::from(openat(
            &current,
            component,
            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
            Mode::empty(),
        )?);
        current = next;
    }

    Ok(current)
}

/// Splits a workspace-relative file path into a parent path and file name.
#[cfg(unix)]
fn split_parent_and_file_name(path: &Path) -> Result<(&Path, &Path), io::Error> {
    let file_name = path
        .file_name()
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?;
    let parent = path.parent().unwrap_or_else(|| Path::new(""));

    Ok((parent, Path::new(file_name)))
}

/// Returns validated normal path components.
#[cfg(unix)]
fn normal_components(path: &Path) -> Result<Vec<&Path>, io::Error> {
    let mut components = Vec::new();
    for component in path.components() {
        match component {
            Component::Normal(part) => components.push(Path::new(part)),
            Component::CurDir => {}
            Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
                return Err(io::Error::new(
                    io::ErrorKind::PermissionDenied,
                    "path contains non-normal component",
                ));
            }
        }
    }

    Ok(components)
}