Compare commits

...
1 Commits
Author SHA1 Message Date
iris 73a32cb897 Add the sixth thing both projects wrote: where state lives
Both servers keep config, the CA's private key and their own state
outside the repo, and both resolve the location by the same rules --
XDG variable, ignored unless absolute, falling back under $HOME, joined
with the product name. ai-app had factored it into one helper taking the
variable and the fallback; dev-updater had the same logic inline in two
functions. That is the test this crate applies: identical but for a
product name.

The reasoning is worth keeping together with the code, so the module doc
carries both halves of why it is outside the repo -- the shared mount
resolves at different absolute paths on each side, which is how every
project once read "not built" on one of them, and a CA key on a mount
the untrusted side can write would let it mint a leaf the pinned app
trusts.

`xdg_dir` takes the environment as an argument so the rules can be
tested without setting process-wide variables, which parallel tests
cannot do without racing. Three tests come across from ai-app, including
the one that matters least often and costs most: a relative setting is
ignored rather than resolved, so a server started from a different
working directory does not quietly look elsewhere for its token hashes
and enroll itself afresh.
2026-08-28 17:33:39 -04:00
2 changed files with 95 additions and 0 deletions

No files matched your search

+1
View File
@@ -32,3 +32,4 @@ pub mod enroll;
pub mod format;
pub mod netif;
pub mod private;
pub mod xdg;
+94
View File
@@ -0,0 +1,94 @@
//! Where each project keeps the state that must not live in its repo.
//!
//! Both servers hold the same three things outside their checkout -- the
//! config with its token hashes, the CA's private key, and whatever state
//! the product itself keeps -- and both resolve the location the same way.
//! That is not a coincidence of style: it is forced by the arrangement the
//! two projects share.
//!
//! The repo is a virtiofs mount shared between a machine and a VM at
//! *different* absolute paths, so a config inside it would record paths
//! that resolve on only one side -- which is exactly how dev-updater came
//! to show "not built" for every project on one of them. And the mount is
//! writable by the untrusted side, so a CA private key inside it would let
//! that side mint a leaf the pinned app trusts, which voids the pinning
//! the rest of this crate exists to provide.
//!
//! Per machine, therefore, and under the XDG directories rather than a
//! path of our own choosing, so that a person's existing backup and
//! sync rules already cover it.
use std::ffi::OsString;
use std::path::PathBuf;
/// `$XDG_CONFIG_HOME/<product>`, or `~/.config/<product>`.
///
/// Holds `config.ron` and `certs/`. `product` is a parameter rather than
/// a constant because the whole point is that two products keep their
/// state apart while resolving it identically.
pub fn config_home(product: &str) -> PathBuf {
xdg_dir(std::env::var_os("XDG_CONFIG_HOME"), ".config", product)
}
/// `$XDG_DATA_HOME/<product>`, or `~/.local/share/<product>`.
///
/// Holds whatever the product accumulates rather than is configured with
/// -- ai-app's session transcripts and attachments, dev-updater's builds.
pub fn data_home(product: &str) -> PathBuf {
xdg_dir(std::env::var_os("XDG_DATA_HOME"), ".local/share", product)
}
/// The resolution both of the above use, with the environment handed in.
///
/// Split out so the rules can be tested without setting process-wide
/// environment variables, which two tests running in parallel cannot do
/// without racing each other.
///
/// A relative value is ignored rather than resolved, per the XDG spec: a
/// server started from a different working directory would otherwise look
/// for its token hashes somewhere new and generate a fresh enrollment,
/// silently locking out the phone that was already enrolled.
fn xdg_dir(base: Option<OsString>, fallback: &str, product: &str) -> PathBuf {
base.map(PathBuf::from)
.filter(|path| path.is_absolute())
.unwrap_or_else(|| {
std::env::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(fallback)
})
.join(product)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_absolute_setting_is_used_and_namespaced_by_product() {
assert_eq!(
xdg_dir(Some("/somewhere".into()), ".config", "ai-app"),
PathBuf::from("/somewhere/ai-app"),
);
assert_eq!(
xdg_dir(Some("/somewhere".into()), ".config", "dev-updater"),
PathBuf::from("/somewhere/dev-updater"),
);
}
#[test]
fn an_unset_value_falls_back_under_home() {
let dir = xdg_dir(None, ".local/share", "ai-app");
assert!(dir.ends_with("ai-app"));
assert!(dir.parent().expect("parent").ends_with("share"));
}
/// A relative setting lands on the same path as no setting at all,
/// rather than on something that moves with the working directory.
#[test]
fn a_relative_setting_is_ignored_rather_than_resolved() {
assert_eq!(
xdg_dir(Some("relative/path".into()), ".config", "ai-app"),
xdg_dir(None, ".config", "ai-app"),
);
}
}