mobius 0.9.26

A small, modular Rust framework for building coding agents
Documentation
//! Capability-safe file reads, ranges, and atomic writes.

use std::io::Read as _;
use std::io::Seek as _;
use std::io::Write as _;
use std::path::Component;
use std::path::Path;

use cap_std::fs::Dir;
use cap_std::fs::OpenOptions;

use super::super::MAX_FILE_BYTES;
use crate::Error;
use crate::Result;

pub(super) fn read_file(root: Dir, relative: &Path, requested: &str) -> Result<String> {
    let file = open_regular_file(root, relative, requested)?;
    let mut bytes = Vec::new();
    file.take(MAX_FILE_BYTES as u64 + 1)
        .read_to_end(&mut bytes)?;
    if bytes.len() > MAX_FILE_BYTES {
        return Err(Error::Sandbox("file exceeds read limit".into()));
    }
    String::from_utf8(bytes).map_err(|_| Error::Sandbox(format!("{requested} is not valid UTF-8")))
}

pub(super) fn read_binary_file(
    root: Dir,
    relative: &Path,
    requested: &str,
    max_bytes: usize,
) -> Result<Vec<u8>> {
    let file = open_regular_file(root, relative, requested)?;
    let mut bytes = Vec::new();
    file.take(max_bytes as u64 + 1).read_to_end(&mut bytes)?;
    if bytes.len() > max_bytes {
        return Err(Error::Sandbox("file exceeds binary read limit".into()));
    }
    Ok(bytes)
}

pub(super) fn read_file_range(
    root: Dir,
    relative: &Path,
    requested: &str,
    offset: u64,
    max_bytes: usize,
) -> Result<(Vec<u8>, Option<u64>)> {
    let mut file = open_regular_file(root, relative, requested)?;
    let size = file.metadata()?.len();
    if offset > size {
        return Err(Error::Sandbox("file offset exceeds its size".into()));
    }
    file.seek(std::io::SeekFrom::Start(offset))?;
    let length = usize::try_from(size.saturating_sub(offset).min(max_bytes as u64))
        .map_err(|_| Error::Sandbox("file range is unsupported".into()))?;
    let mut data = vec![0; length];
    file.read_exact(&mut data)?;
    let end = offset.saturating_add(length as u64);
    Ok((data, (end < size).then_some(end)))
}

fn open_regular_file(root: Dir, relative: &Path, requested: &str) -> Result<cap_std::fs::File> {
    let name = relative
        .file_name()
        .ok_or_else(|| Error::Sandbox(requested.to_string()))?;
    let parent = open_parent(root, relative.parent().unwrap_or(Path::new("")), requested)?;
    let before = parent
        .symlink_metadata(name)
        .map_err(|_| Error::Sandbox(requested.to_string()))?;
    if before.is_symlink() || !before.is_file() {
        return Err(Error::Sandbox(requested.to_string()));
    }
    let file = parent
        .open(name)
        .map_err(|_| Error::Sandbox(requested.to_string()))?;
    let opened = file.metadata()?;
    let current = parent
        .symlink_metadata(name)
        .map_err(|_| Error::Sandbox(requested.to_string()))?;
    if !opened.is_file()
        || current.is_symlink()
        || !same_cap_file(&before, &opened)
        || !same_cap_file(&opened, &current)
    {
        return Err(Error::Sandbox(requested.to_string()));
    }
    Ok(file)
}

pub(super) fn atomic_write(
    root: Dir,
    relative: &Path,
    content: &[u8],
    requested: &str,
) -> Result<()> {
    let target = relative
        .file_name()
        .ok_or_else(|| Error::Sandbox(requested.to_string()))?;
    let parent = open_parent(root, relative.parent().unwrap_or(Path::new("")), requested)?;
    let permissions = match parent.symlink_metadata(target) {
        Ok(metadata) if metadata.is_symlink() || !metadata.is_file() => {
            return Err(Error::Sandbox(requested.to_string()));
        }
        Ok(metadata) => Some(metadata.permissions()),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
        Err(error) => return Err(error.into()),
    };
    let temporary = format!(".mobius-write-{}.tmp", uuid::Uuid::new_v4());
    let result = (|| {
        let mut options = OpenOptions::new();
        options.write(true).create_new(true);
        let mut file = parent.open_with(&temporary, &options)?;
        if let Some(permissions) = permissions {
            file.set_permissions(permissions)?;
        }
        file.write_all(content)?;
        file.sync_all()?;
        drop(file);
        parent.rename(&temporary, &parent, target)?;
        sync_directory(&parent)?;
        Ok(())
    })();
    if result.is_err() {
        let _ = parent.remove_file(&temporary);
    }
    result
}

fn sync_directory(directory: &Dir) -> Result<()> {
    directory.open(".")?.sync_all()?;
    Ok(())
}
fn open_parent(mut parent: Dir, path: &Path, requested: &str) -> Result<Dir> {
    for component in path.components() {
        let Component::Normal(name) = component else {
            continue;
        };
        let before = parent
            .symlink_metadata(name)
            .map_err(|_| Error::Sandbox(requested.to_string()))?;
        if before.is_symlink() || !before.is_dir() {
            return Err(Error::Sandbox(requested.to_string()));
        }
        let next = parent
            .open_dir(name)
            .map_err(|_| Error::Sandbox(requested.to_string()))?;
        if !same_cap_file(&before, &next.dir_metadata()?) {
            return Err(Error::Sandbox(requested.to_string()));
        }
        parent = next;
    }
    Ok(parent)
}

#[cfg(unix)]
fn same_cap_file(left: &cap_std::fs::Metadata, right: &cap_std::fs::Metadata) -> bool {
    use cap_std::fs::MetadataExt;

    left.dev() == right.dev() && left.ino() == right.ino()
}

#[cfg(not(unix))]
fn same_cap_file(_left: &cap_std::fs::Metadata, _right: &cap_std::fs::Metadata) -> bool {
    false
}