memoryd: the crate, memoryd.toml, and listening on a socket
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
//! `memoryd.toml`: defaults, paths under `home`, the starter next to the config file, and unknown
|
||||
//! keys refused (M5a spec, section 11). Do not edit.
|
||||
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use memoryd::config::Config;
|
||||
use proto::DataClass;
|
||||
use tmp::TempDir;
|
||||
|
||||
#[test]
|
||||
fn defaults_and_paths_under_home() {
|
||||
let config = Config::parse("[paths]\nhome = \"/srv/bx\"\n").unwrap();
|
||||
assert!(!config.memory.review);
|
||||
assert_eq!(config.index.max_class, DataClass::Private);
|
||||
assert_eq!(
|
||||
config.loop_socket(),
|
||||
PathBuf::from("/srv/bx/run/loop-memory/memory.sock")
|
||||
);
|
||||
assert_eq!(
|
||||
config.broker_socket(),
|
||||
PathBuf::from("/srv/bx/run/broker-memory/memory.sock")
|
||||
);
|
||||
assert_eq!(
|
||||
config.admin_socket(),
|
||||
PathBuf::from("/srv/bx/run/owner-memory/admin.sock")
|
||||
);
|
||||
assert_eq!(config.memory_dir(), PathBuf::from("/srv/bx/memory"));
|
||||
assert_eq!(
|
||||
config.index_path(),
|
||||
PathBuf::from("/srv/bx/index/memory.sqlite3")
|
||||
);
|
||||
assert_eq!(config.sessions_dir(), PathBuf::from("/srv/bx/sessions"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_setting_can_be_given() {
|
||||
let text = "[paths]\nhome = \"/h\"\n[memory]\nsoul_starter = \"/etc/bx/soul.md\"\nreview = true\n[index]\nmax_class = \"secret\"\n[sockets]\nloop = \"/r/l.sock\"\nbroker = \"/r/b.sock\"\nadmin = \"/r/a.sock\"\n";
|
||||
let config = Config::parse(text).unwrap();
|
||||
assert!(config.memory.review);
|
||||
assert_eq!(config.index.max_class, DataClass::Secret);
|
||||
assert_eq!(config.memory.soul_starter, PathBuf::from("/etc/bx/soul.md"));
|
||||
assert_eq!(
|
||||
(
|
||||
config.loop_socket(),
|
||||
config.broker_socket(),
|
||||
config.admin_socket()
|
||||
),
|
||||
(
|
||||
PathBuf::from("/r/l.sock"),
|
||||
PathBuf::from("/r/b.sock"),
|
||||
PathBuf::from("/r/a.sock")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_keys_are_refused_in_every_table() {
|
||||
for text in [
|
||||
"[paths]\nhomee = \"/h\"\n",
|
||||
"[memory]\nreveiw = true\n",
|
||||
"[index]\nmax = \"private\"\n",
|
||||
"[sockets]\nloopd = \"/x\"\n",
|
||||
"[extra]\n",
|
||||
"[index]\nmax_class = \"top-secret\"\n",
|
||||
] {
|
||||
assert!(Config::parse(text).is_err(), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_relative_starter_is_next_to_the_config_file() {
|
||||
let dir = TempDir::new("config");
|
||||
let path = dir.write("etc/memoryd.toml", "[memory]\nsoul_starter = \"soul.md\"\n");
|
||||
let config = Config::load(&path).unwrap();
|
||||
assert_eq!(config.memory.soul_starter, dir.path().join("etc/soul.md"));
|
||||
let path = dir.write("etc/default.toml", "");
|
||||
let config = Config::load(&path).unwrap();
|
||||
assert_eq!(config.memory.soul_starter, dir.path().join("etc/soul.md"));
|
||||
let path = dir.write(
|
||||
"etc/abs.toml",
|
||||
"[memory]\nsoul_starter = \"/opt/soul.md\"\n",
|
||||
);
|
||||
let config = Config::load(&path).unwrap();
|
||||
assert_eq!(config.memory.soul_starter, PathBuf::from("/opt/soul.md"));
|
||||
let missing = Config::load(&dir.path().join("nope.toml")).unwrap_err();
|
||||
assert!(missing.to_string().contains("nope.toml"), "{missing}");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//! `listen`: the directory at 0700, the socket at 0600, a stale socket replaced, and no mode
|
||||
//! changed through a symbolic link (M5a spec, section 7). Do not edit.
|
||||
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use memoryd::socket::listen;
|
||||
use tmp::TempDir;
|
||||
|
||||
fn mode(path: &std::path::Path) -> u32 {
|
||||
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_and_socket_are_private_and_a_stale_socket_is_replaced() {
|
||||
let dir = TempDir::new("socket");
|
||||
let run = dir.path().join("run/loop-memory");
|
||||
std::fs::create_dir_all(&run).unwrap();
|
||||
std::fs::set_permissions(&run, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let socket = run.join("memory.sock");
|
||||
let first = listen(&socket).unwrap();
|
||||
assert_eq!((mode(&run), mode(&socket)), (0o700, 0o600));
|
||||
drop(first);
|
||||
let second = listen(&socket).expect("a stale socket is removed");
|
||||
assert!(UnixStream::connect(&socket).is_ok());
|
||||
drop(second);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_directory_and_symbolic_links_are_refused() {
|
||||
let dir = TempDir::new("socket-link");
|
||||
let target = dir.path().join("shared");
|
||||
std::fs::create_dir_all(&target).unwrap();
|
||||
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
std::os::unix::fs::symlink(&target, dir.path().join("link")).unwrap();
|
||||
let err = listen(&dir.path().join("link/memory.sock")).unwrap_err();
|
||||
assert!(err.contains("symbolic link"), "{err}");
|
||||
assert_eq!(mode(&target), 0o755, "the target's mode is untouched");
|
||||
let err = listen(std::path::Path::new("/memory.sock")).unwrap_err();
|
||||
assert!(err.contains("directory of its own"), "{err}");
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Temporary directories for tests. Do not edit.
|
||||
|
||||
#![allow(dead_code)] // each test file uses a different part of this module
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub struct TempDir(PathBuf);
|
||||
|
||||
impl TempDir {
|
||||
pub fn new(tag: &str) -> TempDir {
|
||||
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
||||
let path = std::env::temp_dir().join(format!("md-{tag}-{}-{n}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
TempDir(path)
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Writes `text` to `name` inside the directory and returns the full path.
|
||||
pub fn write(&self, name: &str, text: &str) -> PathBuf {
|
||||
let path = self.0.join(name);
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
std::fs::write(&path, text).unwrap();
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user