agentmux_launcher/
hash.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Stable 64-bit hash for deriving per-data-dir IPC names. Used to
5// build the named-pipe path `\\.\pipe\agentmux-{hash16}\command`
6// so each AgentMux instance (per CLAUDE.md, multiple parallel
7// instances are supported per-data-dir) gets a distinct IPC
8// surface, kernel-isolated from siblings.
9//
10// We hand-roll FNV-1a because:
11//   * Adding `sha2` for ~64 bits of stable hash is overkill (3+ MB
12//     deps) when this is non-cryptographic.
13//   * `std::collections::hash_map::DefaultHasher` is explicitly
14//     NOT documented as stable across runs / Rust versions; we
15//     need stability so the same launcher binary always picks the
16//     same pipe name for the same data dir.
17//   * FNV-1a is deterministic, well-known, and ~20 lines of code.
18//
19// Collisions are non-cryptographic but adequate for this scope:
20// the hash inputs are filesystem paths, the keyspace is tiny, and
21// a collision just means two installs at different paths share a
22// pipe name — they'd need to be running simultaneously AND have
23// the SAME data dir hash, which is astronomically unlikely with
24// 16 hex chars (64 bits) of namespace.
25
26const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
27const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
28
29/// 64-bit FNV-1a hash of bytes. Stable across runs.
30pub fn fnv1a_64(bytes: &[u8]) -> u64 {
31    let mut hash = FNV_OFFSET_BASIS;
32    for b in bytes {
33        hash ^= *b as u64;
34        hash = hash.wrapping_mul(FNV_PRIME);
35    }
36    hash
37}
38
39/// First 16 hex chars of FNV-1a-64 over the canonical-lowercase
40/// representation of the data_dir path. Used as the per-instance
41/// IPC namespace component.
42pub fn data_dir_hash16(data_dir: &std::path::Path) -> String {
43    // Canonicalize if possible (resolves `..`, mixed casing on
44    // Windows), but fall back to the raw path if canonicalize fails
45    // (e.g. data_dir doesn't exist yet during early startup).
46    let canonical = data_dir
47        .canonicalize()
48        .unwrap_or_else(|_| data_dir.to_path_buf());
49    let s = canonical.to_string_lossy().to_lowercase();
50    format!("{:016x}", fnv1a_64(s.as_bytes()))
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn fnv1a_known_vector() {
59        // Standard test vector for FNV-1a-64 on empty input.
60        assert_eq!(fnv1a_64(b""), FNV_OFFSET_BASIS);
61        // From http://www.isthe.com/chongo/tech/comp/fnv/test_vectors.html
62        // ("foobar" → 0x85944171f73967e8)
63        assert_eq!(fnv1a_64(b"foobar"), 0x85944171f73967e8);
64    }
65
66    #[test]
67    fn data_dir_hash_stable_across_calls() {
68        let p = std::path::PathBuf::from("C:\\Users\\test\\AgentMux");
69        assert_eq!(data_dir_hash16(&p), data_dir_hash16(&p));
70        assert_eq!(data_dir_hash16(&p).len(), 16);
71    }
72
73    #[test]
74    fn data_dir_hash_case_insensitive() {
75        // Windows paths shouldn't produce different hashes for
76        // different casings of the same logical path.
77        let lower = std::path::PathBuf::from("c:\\users\\test");
78        let upper = std::path::PathBuf::from("C:\\Users\\Test");
79        assert_eq!(data_dir_hash16(&lower), data_dir_hash16(&upper));
80    }
81}