Codex Handbook
ext/memories/src/local.rs 129 lines
use std::path::Component;use std::path::Path;use std::path::PathBuf;use codex_utils_absolute_path::AbsolutePathBuf;use crate::backend::AddAdHocMemoryNoteRequest;use crate::backend::AddAdHocMemoryNoteResponse;use crate::backend::ListMemoriesRequest;use crate::backend::ListMemoriesResponse;use crate::backend::MemoriesBackend;use crate::backend::MemoriesBackendError;use crate::backend::ReadMemoryRequest;use crate::backend::ReadMemoryResponse;use crate::backend::SearchMemoriesRequest;use crate::backend::SearchMemoriesResponse;mod ad_hoc_note;mod list;mod path;mod read;mod search;#[derive(Debug, Clone)]pub(crate) struct LocalMemoriesBackend {    root: PathBuf,}impl LocalMemoriesBackend {    pub(crate) fn from_codex_home(codex_home: &AbsolutePathBuf) -> Self {        Self::from_memory_root(codex_home.join("memories").to_path_buf())    }    pub(crate) fn from_memory_root(root: impl Into<PathBuf>) -> Self {        Self { root: root.into() }    }    async fn resolve_scoped_path(        &self,        relative_path: Option<&str>,    ) -> Result<PathBuf, MemoriesBackendError> {        let Some(relative_path) = relative_path else {            return Ok(self.root.clone());        };        let relative = Path::new(relative_path);        if relative.components().any(|component| {            matches!(                component,                Component::ParentDir | Component::RootDir | Component::Prefix(_)            )        }) {            return Err(MemoriesBackendError::invalid_path(                relative_path,                "must stay within the memories root",            ));        }        if relative.components().any(path::is_hidden_component) {            return Err(MemoriesBackendError::NotFound {                path: relative_path.to_string(),            });        }        let components = relative.components().collect::<Vec<_>>();        let mut scoped_path = self.root.clone();        for (idx, component) in components.iter().enumerate() {            scoped_path.push(component.as_os_str());            let Some(metadata) = Self::metadata_or_none(&scoped_path).await? else {                for remaining_component in components.iter().skip(idx + 1) {                    scoped_path.push(remaining_component.as_os_str());                }                return Ok(scoped_path);            };            path::reject_symlink(                &path::display_relative_path(&self.root, &scoped_path),                &metadata,            )?;            if idx + 1 < components.len() && !metadata.is_dir() {                return Err(MemoriesBackendError::invalid_path(                    relative_path,                    "traverses through a non-directory path component",                ));            }        }        Ok(scoped_path)    }    async fn metadata_or_none(        path: &Path,    ) -> Result<Option<std::fs::Metadata>, MemoriesBackendError> {        match tokio::fs::symlink_metadata(path).await {            Ok(metadata) => Ok(Some(metadata)),            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),            Err(err) => Err(err.into()),        }    }}impl MemoriesBackend for LocalMemoriesBackend {    async fn add_ad_hoc_note(        &self,        request: AddAdHocMemoryNoteRequest,    ) -> Result<AddAdHocMemoryNoteResponse, MemoriesBackendError> {        ad_hoc_note::add_ad_hoc_note(self, request).await    }    async fn list(        &self,        request: ListMemoriesRequest,    ) -> Result<ListMemoriesResponse, MemoriesBackendError> {        list::list(self, request).await    }    async fn read(        &self,        request: ReadMemoryRequest,    ) -> Result<ReadMemoryResponse, MemoriesBackendError> {        read::read(self, request).await    }    async fn search(        &self,        request: SearchMemoriesRequest,    ) -> Result<SearchMemoriesResponse, MemoriesBackendError> {        search::search(self, request).await    }}