9 Commits
Author SHA1 Message Date
Andy Dixon b06094b2aa Tests: skip Go-oracle CLI interop on Windows
The frozen Go version cannot sync directories there.
2026-10-13 14:47:30 +01:00
Andy Dixon c9725f18d4 Tests on Windows: no console for CLI children, .exe for the Go oracle; CI test timeout
A hidden console on Windows runners let a confirmation prompt wait
forever, and the oracle built without .exe could not be run.
2026-10-13 14:23:43 +01:00
Andy Dixon f62e74a3e9 0.6.1 2026-10-13 12:55:23 +01:00
Andy Dixon 1811ff4d33 Tests: find agents with pgrep, since macOS has no /proc 2026-10-13 12:50:55 +01:00
Andy Dixon 3347efe418 CI: keep test vectors byte-exact on Windows, short agent paths on macOS
Windows checkouts converted LF vectors to CRLF; SystemTime there has
100 ns resolution. macOS's long $TMPDIR pushed the agent socket past
SUN_LEN.
2026-10-13 12:42:23 +01:00
Andy DixonandClaude e8f21f68a6 Guide: scale the taller help screenshot to fit its page
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-08 19:15:00 +01:00
Andy DixonandClaude 660e9cd5bf Guide: split the error table so it fits the page; PLATFORMS: no Homebrew yet
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-08 19:12:00 +01:00
Andy Dixon 404aa91573 Merge branch 'rust-port': screenshots and macOS cross-build support 2026-10-08 18:28:04 +01:00
Andy Dixon 0ebf6ce97c Merge pull request #1 from andydixon/rust-port
Rewrite vault in Rust (format-compatible with 0.5)
2026-10-08 17:38:32 +01:00
10 changed files with 53 additions and 37 deletions
+2
View File
@@ -0,0 +1,2 @@
# Test vectors are compared byte for byte; never convert line endings.
tests/vectors/** -text
+1
View File
@@ -27,6 +27,7 @@ jobs:
matrix: matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel, windows-2025] os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel, windows-2025]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
# The Go toolchain builds the test-only oracle; vault itself needs no Go. # The Go toolchain builds the test-only oracle; vault itself needs no Go.
Generated
+1 -1
View File
@@ -1132,7 +1132,7 @@ checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]] [[package]]
name = "vault" name = "vault"
version = "0.6.0" version = "0.6.1"
dependencies = [ dependencies = [
"aes", "aes",
"argon2", "argon2",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "vault" name = "vault"
version = "0.6.0" version = "0.6.1"
edition = "2024" edition = "2024"
rust-version = "1.99" rust-version = "1.99"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
+2 -2
View File
@@ -51,8 +51,8 @@ disables core dumps. See `SECURITY.md`.
on Linux cross-builds macOS with `--no-default-features`: those binaries on Linux cross-builds macOS with `--no-default-features`: those binaries
verify TLS for `add URL` and `mirror` against the bundled Mozilla roots verify TLS for `add URL` and `mirror` against the bundled Mozilla roots
instead of the macOS keychain (so a corporate CA added to the keychain is instead of the macOS keychain (so a corporate CA added to the keychain is
not trusted). Built on a Mac, or through Homebrew not trusted). Built on a Mac (`cargo build --release`), vault uses the
(`brew install andydixon/tap/vault`), they use the keychain. keychain.
## No longer released ## No longer released
+12 -6
View File
@@ -297,7 +297,7 @@ $ chmod +x vault-linux-arm64 && mv vault-linux-arm64 $PREFIX/bin/vault
Run \cmd{vault help}. You should see the full command list: Run \cmd{vault help}. You should see the full command list:
\shot{help}{\cmd{vault help}: everything vault can do, on one screen} \shot[0.95]{help}{\cmd{vault help}: everything vault can do, on one screen}
\section{Building it yourself} \section{Building it yourself}
@@ -1332,6 +1332,17 @@ could not read passphrase (set VAULT\_PASSPHRASE\ldots) & No terminal to ask on,
agent did not start: \ldots & \cmd{unlock} couldn't start its helper. The reason follows; commands still work, asking for the passphrase. \\ agent did not start: \ldots & \cmd{unlock} couldn't start its helper. The reason follows; commands still work, asking for the passphrase. \\
nothing was unlocked & \cmd{vault lock} found nothing to lock. Not an error. \\ nothing was unlocked & \cmd{vault lock} found nothing to lock. Not an error. \\
waiting for another vault process to finish & Not an error. Another command is writing; this one continues when it's done. \\ waiting for another vault process to finish & Not an error. Another command is writing; this one continues when it's done. \\
skipping unreadable record \ldots & A catalogue entry is damaged. Everything else still works; restore \file{records/} from backup to recover that file. \\
object does not match its catalogue record & The encrypted contents were swapped or damaged. Nothing was written. Restore \file{objects/} from backup. \\
object size does not match its catalogue record & Same as above: damage or tampering, caught before any output appeared. \\
cannot find home directory; set VAULT\_KEY & Unusual environments only. Set \env{VAULT\_KEY} and \env{VAULT\_DIR}. \\
unknown command / wrong number of arguments & A typo in the command itself. The usage text follows. \\
\end{vtable}
\subsection{Files, the lock and downloads}
\begin{vtable}{>{\ttfamily\footnotesize}p{58mm}Y}
\thead{MESSAGE} & \thead{WHAT IT MEANS, AND THE FIX} \\ \midrule
cannot lock the vault: \ldots & The lock file couldn't be opened or locked; the reason follows. Check the vault folder's permissions. \\ cannot lock the vault: \ldots & The lock file couldn't be opened or locked; the reason follows. Check the vault folder's permissions. \\
\ldots\ changed while being added & The file was being written while vault read it. Nothing was stored; try again when it's finished. \\ \ldots\ changed while being added & The file was being written while vault read it. Nothing was stored; try again when it's finished. \\
\ldots\ changed after it was stored; not deleting it & \cmd{migrate} stored the file, but it changed before it could be deleted. The original is kept; the vault has the version it read. \\ \ldots\ changed after it was stored; not deleting it & \cmd{migrate} stored the file, but it changed before it could be deleted. The original is kept; the vault has the version it read. \\
@@ -1342,11 +1353,6 @@ vault unlock is not supported on this platform\ldots & Windows: every command as
download exceeds \ldots\ bytes & A download passed its size limit. Raise it with \cmd{--max-bytes} if you meant it. \\ download exceeds \ldots\ bytes & A download passed its size limit. Raise it with \cmd{--max-bytes} if you meant it. \\
redirected to another host: \ldots & \cmd{mirror} only follows redirects within the site it is copying. \\ redirected to another host: \ldots & \cmd{mirror} only follows redirects within the site it is copying. \\
mirror incomplete: stored \ldots, skipped \ldots & Some pages failed; the reasons were printed as they happened. Exit status 3. \\ mirror incomplete: stored \ldots, skipped \ldots & Some pages failed; the reasons were printed as they happened. Exit status 3. \\
skipping unreadable record \ldots & A catalogue entry is damaged. Everything else still works; restore \file{records/} from backup to recover that file. \\
object does not match its catalogue record & The encrypted contents were swapped or damaged. Nothing was written. Restore \file{objects/} from backup. \\
object size does not match its catalogue record & Same as above: damage or tampering, caught before any output appeared. \\
cannot find home directory; set VAULT\_KEY & Unusual environments only. Set \env{VAULT\_KEY} and \env{VAULT\_DIR}. \\
unknown command / wrong number of arguments & A typo in the command itself. The usage text follows. \\
\end{vtable} \end{vtable}
\section{``My files have vanished!''} \section{``My files have vanished!''}
+1 -1
View File
@@ -1091,7 +1091,7 @@ checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]] [[package]]
name = "vault" name = "vault"
version = "0.6.0" version = "0.6.1"
dependencies = [ dependencies = [
"argon2", "argon2",
"base64", "base64",
+3 -2
View File
@@ -526,12 +526,13 @@ mod tests {
); );
assert_eq!(Instant::parse_rfc3339("2023-02-29T00:00:00Z"), None); assert_eq!(Instant::parse_rfc3339("2023-02-29T00:00:00Z"), None);
assert!(Instant::parse_rfc3339("2024-02-29T00:00:00Z").is_some()); assert!(Instant::parse_rfc3339("2024-02-29T00:00:00Z").is_some());
let neg = Instant::from_system(std::time::UNIX_EPOCH - Duration::new(0, 1)); // 100 ns: Windows' SystemTime resolution.
let neg = Instant::from_system(std::time::UNIX_EPOCH - Duration::new(0, 100));
assert_eq!( assert_eq!(
neg, neg,
Instant { Instant {
secs: -1, secs: -1,
nanos: 999_999_999 nanos: 999_999_900
} }
); );
assert_eq!(Instant::from_system(neg.to_system()), neg); assert_eq!(Instant::from_system(neg.to_system()), neg);
+24 -22
View File
@@ -24,7 +24,13 @@ struct Env {
impl Env { impl Env {
fn new(tag: &str) -> Env { fn new(tag: &str) -> Env {
let dir = std::env::temp_dir().join(format!( // Not temp_dir(): macOS's long $TMPDIR pushes the agent socket path
// past SUN_LEN.
#[cfg(unix)]
let base = PathBuf::from("/tmp");
#[cfg(not(unix))]
let base = std::env::temp_dir();
let dir = base.join(format!(
"vault-cli-{tag}-{}-{}", "vault-cli-{tag}-{}-{}",
std::process::id(), std::process::id(),
std::time::SystemTime::now() std::time::SystemTime::now()
@@ -48,6 +54,10 @@ impl Env {
if let Some(p) = pass { if let Some(p) = pass {
c.env("VAULT_PASSPHRASE", p); c.env("VAULT_PASSPHRASE", p);
} }
// No console, as Unix tests have no terminal: CI runners have a
// hidden one, where a prompt would wait forever.
#[cfg(windows)]
std::os::windows::process::CommandExt::creation_flags(&mut c, 0x0000_0008); // DETACHED_PROCESS
c c
} }
@@ -152,7 +162,7 @@ fn rust_lifecycle() {
#[test] #[test]
fn go_made_rust_used() { fn go_made_rust_used() {
let Some(go) = common::go_oracle() else { let Some(go) = common::go_oracle() else {
return common::skip("go not installed"); return common::skip("no Go oracle");
}; };
lifecycle(go.to_str().unwrap(), RUST); lifecycle(go.to_str().unwrap(), RUST);
} }
@@ -160,7 +170,7 @@ fn go_made_rust_used() {
#[test] #[test]
fn rust_made_go_used() { fn rust_made_go_used() {
let Some(go) = common::go_oracle() else { let Some(go) = common::go_oracle() else {
return common::skip("go not installed"); return common::skip("no Go oracle");
}; };
lifecycle(RUST, go.to_str().unwrap()); lifecycle(RUST, go.to_str().unwrap());
} }
@@ -169,7 +179,7 @@ fn rust_made_go_used() {
#[test] #[test]
fn boundary_sizes_interop() { fn boundary_sizes_interop() {
let Some(go) = common::go_oracle() else { let Some(go) = common::go_oracle() else {
return common::skip("go not installed"); return common::skip("no Go oracle");
}; };
let go = go.to_str().unwrap(); let go = go.to_str().unwrap();
let e = Env::new("bounds"); let e = Env::new("bounds");
@@ -382,7 +392,7 @@ fn lock_contention(holder: &str, waiter: &str) {
#[test] #[test]
fn lock_rust_waits_for_go() { fn lock_rust_waits_for_go() {
let Some(go) = common::go_oracle() else { let Some(go) = common::go_oracle() else {
return common::skip("go not installed"); return common::skip("no Go oracle");
}; };
lock_contention(go.to_str().unwrap(), RUST); lock_contention(go.to_str().unwrap(), RUST);
} }
@@ -391,7 +401,7 @@ fn lock_rust_waits_for_go() {
#[test] #[test]
fn lock_go_waits_for_rust() { fn lock_go_waits_for_rust() {
let Some(go) = common::go_oracle() else { let Some(go) = common::go_oracle() else {
return common::skip("go not installed"); return common::skip("no Go oracle");
}; };
lock_contention(RUST, go.to_str().unwrap()); lock_contention(RUST, go.to_str().unwrap());
} }
@@ -431,7 +441,7 @@ fn agent_rust() {
#[test] #[test]
fn agent_go_client_rust_agent() { fn agent_go_client_rust_agent() {
let Some(go) = common::go_oracle() else { let Some(go) = common::go_oracle() else {
return common::skip("go not installed"); return common::skip("no Go oracle");
}; };
agent_roundtrip(RUST, go.to_str().unwrap()); agent_roundtrip(RUST, go.to_str().unwrap());
} }
@@ -440,27 +450,19 @@ fn agent_go_client_rust_agent() {
#[test] #[test]
fn agent_rust_client_go_agent() { fn agent_rust_client_go_agent() {
let Some(go) = common::go_oracle() else { let Some(go) = common::go_oracle() else {
return common::skip("go not installed"); return common::skip("no Go oracle");
}; };
agent_roundtrip(go.to_str().unwrap(), RUST); agent_roundtrip(go.to_str().unwrap(), RUST);
} }
#[cfg(unix)] #[cfg(unix)]
fn agent_pids(sock_dir: &Path) -> Vec<u32> { fn agent_pids(sock_dir: &Path) -> Vec<u32> {
let mut pids = Vec::new(); // pgrep, not /proc: macOS has no /proc.
for e in std::fs::read_dir("/proc").into_iter().flatten().flatten() { let o = Command::new("pgrep")
let Ok(pid) = e.file_name().to_string_lossy().parse::<u32>() else { .args(["-f", &format!("__agent {}", sock_dir.display())])
continue; .output()
}; .unwrap();
let Ok(cmd) = std::fs::read(e.path().join("cmdline")) else { String::from_utf8_lossy(&o.stdout).lines().filter_map(|l| l.parse().ok()).collect()
continue;
};
let cmd = String::from_utf8_lossy(&cmd);
if cmd.contains("__agent") && cmd.contains(&*sock_dir.to_string_lossy()) {
pids.push(pid);
}
}
pids
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
+6 -2
View File
@@ -31,10 +31,14 @@ pub fn go_oracle() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("VAULT_GO_ORACLE") { if let Some(p) = std::env::var_os("VAULT_GO_ORACLE") {
return Some(PathBuf::from(p)); return Some(PathBuf::from(p));
} }
if !have_go() { // The frozen Go version cannot sync directories on Windows ("Access
// is denied"), so its vaults cannot be shared there.
if cfg!(windows) || !have_go() {
return None; return None;
} }
let out = root().join("target").join("vault-go-oracle"); let out = root()
.join("target")
.join(format!("vault-go-oracle{}", std::env::consts::EXE_SUFFIX));
let st = Command::new("go") let st = Command::new("go")
.args(["build", "-o"]) .args(["build", "-o"])
.arg(&out) .arg(&out)