manifest.rs (4718B)
1 //! The driver manifest: how the daemon tells the Hydra HAL driver which virtual 2 //! devices to publish. 3 //! 4 //! The driver runs inside `coreaudiod` (as `_coreaudiod`), which cannot read the user's 5 //! home directory — so the manifest lives at a world-readable system path. The daemon 6 //! writes it; the driver reads it at load (and, in P6, on a live-reload kick). 7 //! 8 //! P2 honours only the **first** device's `name`/`uid` (channels are fixed at the driver's 9 //! build-time channel count). The full N-device array is P3. 10 11 use std::io; 12 use std::path::{Path, PathBuf}; 13 14 use serde::{Deserialize, Serialize}; 15 16 /// World-readable directory both the daemon and the sandboxed driver can reach. 17 pub const MANIFEST_DIR: &str = "/Library/Application Support/hydra"; 18 /// The manifest file name within [`MANIFEST_DIR`]. 19 pub const MANIFEST_FILE: &str = "devices.json"; 20 21 /// Current manifest schema version (bumped on incompatible changes). 22 pub const MANIFEST_VERSION: u32 = 1; 23 24 /// One virtual device the driver should publish. 25 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 26 pub struct ManifestDevice { 27 /// Stable UID other apps use to select the device (e.g. "hydra:main"). 28 pub uid: String, 29 /// Human-readable name shown in audio device pickers. 30 pub name: String, 31 /// Channel count. Advisory in P2 (driver uses its build-time count); honoured in P3. 32 pub channels: u32, 33 /// Supported sample rates; empty ⇒ driver defaults. 34 #[serde(default)] 35 pub sample_rates: Vec<u32>, 36 } 37 38 /// The full manifest document written to disk. 39 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 40 pub struct Manifest { 41 pub version: u32, 42 pub devices: Vec<ManifestDevice>, 43 } 44 45 impl Manifest { 46 pub fn new(devices: Vec<ManifestDevice>) -> Self { 47 Self { version: MANIFEST_VERSION, devices } 48 } 49 50 /// Canonical manifest path (`/Library/Application Support/hydra/devices.json`). 51 pub fn path() -> PathBuf { 52 Path::new(MANIFEST_DIR).join(MANIFEST_FILE) 53 } 54 55 /// Serialize to pretty JSON (the driver's tiny parser tolerates whitespace). 56 pub fn to_json(&self) -> String { 57 // Infallible for this plain struct; fall back to an empty doc rather than panic. 58 serde_json::to_string_pretty(self).unwrap_or_else(|_| { 59 format!("{{\"version\":{MANIFEST_VERSION},\"devices\":[]}}") 60 }) 61 } 62 63 /// Write the manifest to `path`, creating the parent directory if possible. 64 /// 65 /// Writing under `/Library/Application Support` needs privileges the daemon may not 66 /// have until the installer has created+chowned the directory; callers should treat 67 /// an `io::Error` here as "driver not installed yet", not fatal. 68 pub fn write_to(&self, path: &Path) -> io::Result<()> { 69 if let Some(dir) = path.parent() { 70 std::fs::create_dir_all(dir)?; 71 } 72 std::fs::write(path, self.to_json()) 73 } 74 75 /// Write to the canonical [`Manifest::path`]. 76 pub fn write(&self) -> io::Result<()> { 77 self.write_to(&Self::path()) 78 } 79 } 80 81 #[cfg(test)] 82 mod tests { 83 use super::*; 84 85 fn sample() -> Manifest { 86 Manifest::new(vec![ManifestDevice { 87 uid: "hydra:main".into(), 88 name: "Hydra Main".into(), 89 channels: 16, 90 sample_rates: vec![44100, 48000], 91 }]) 92 } 93 94 #[test] 95 fn round_trips_through_json() { 96 let m = sample(); 97 let parsed: Manifest = serde_json::from_str(&m.to_json()).unwrap(); 98 assert_eq!(m, parsed); 99 } 100 101 #[test] 102 fn json_has_expected_shape() { 103 let json = sample().to_json(); 104 assert!(json.contains("\"version\": 1")); 105 assert!(json.contains("\"uid\": \"hydra:main\"")); 106 assert!(json.contains("\"name\": \"Hydra Main\"")); 107 } 108 109 #[test] 110 fn sample_rates_default_when_absent() { 111 let parsed: Manifest = 112 serde_json::from_str(r#"{"version":1,"devices":[{"uid":"u","name":"n","channels":2}]}"#).unwrap(); 113 assert!(parsed.devices[0].sample_rates.is_empty()); 114 } 115 116 #[test] 117 fn writes_and_reads_back_from_tempdir() { 118 let dir = std::env::temp_dir().join(format!("hydra-manifest-test-{}", std::process::id())); 119 let path = dir.join("devices.json"); 120 let m = sample(); 121 m.write_to(&path).unwrap(); 122 let back: Manifest = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); 123 assert_eq!(m, back); 124 let _ = std::fs::remove_dir_all(&dir); 125 } 126 127 #[test] 128 fn canonical_path_is_system_location() { 129 assert_eq!( 130 Manifest::path().to_str().unwrap(), 131 "/Library/Application Support/hydra/devices.json" 132 ); 133 } 134 }