1use serde::{Deserialize, Serialize};
17
18use super::common::{DriveId, SafePath};
19
20#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)]
22pub enum CacheType {
23 #[default]
25 Unsafe,
26 Writeback,
28}
29
30#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)]
32pub enum IoEngine {
33 #[default]
35 Sync,
36 Async,
38}
39
40#[derive(Debug, Clone, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct RawDriveConfig {
44 pub drive_id: String,
46 pub path_on_host: String,
48 #[serde(default)]
50 pub is_root_device: bool,
51 #[serde(default)]
53 pub is_read_only: bool,
54 #[serde(default)]
56 pub cache_type: CacheType,
57 #[serde(default)]
59 pub io_engine: IoEngine,
60 #[serde(default)]
62 pub partuuid: Option<String>,
63 #[serde(default)]
65 pub rate_limiter: Option<serde_json::Value>,
66 #[serde(default)]
68 pub socket: Option<String>,
69}
70
71#[derive(Debug, Clone, Serialize)]
73#[non_exhaustive]
74pub struct DriveConfig {
75 pub drive_id: DriveId,
77 pub path_on_host: SafePath,
79 pub is_root_device: bool,
81 pub is_read_only: bool,
83 pub cache_type: CacheType,
85 pub io_engine: IoEngine,
87 pub partuuid: Option<String>,
89 pub rate_limiter: Option<serde_json::Value>,
91 pub vhost_user_socket: Option<String>,
93}
94
95fn validate_partuuid(uuid: &str) -> Result<(), String> {
96 if uuid.is_empty() || uuid.len() > 64 {
97 return Err(format!(
98 "Invalid partuuid: must be 1..=64 bytes (got {} bytes)",
99 uuid.len()
100 ));
101 }
102 if !uuid.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') {
103 return Err("Invalid partuuid: only [A-Za-z0-9-] permitted".into());
104 }
105 Ok(())
106}
107
108impl TryFrom<RawDriveConfig> for DriveConfig {
109 type Error = String;
110
111 fn try_from(raw: RawDriveConfig) -> Result<Self, Self::Error> {
112 let drive_id = DriveId::new(raw.drive_id)?;
113 let path_on_host =
114 SafePath::new(raw.path_on_host).map_err(|e| format!("Invalid path_on_host: {e}"))?;
115 if let Some(p) = raw.partuuid.as_deref() {
116 validate_partuuid(p)?;
117 }
118 let socket = match raw.socket {
119 Some(s) if s.is_empty() => return Err("Invalid socket: must not be empty".into()),
120 Some(s) if s.len() > 1024 => {
121 return Err("Invalid socket: exceeds 1024 bytes".into());
122 }
123 other => other,
124 };
125 Ok(Self {
126 drive_id,
127 path_on_host,
128 is_root_device: raw.is_root_device,
129 is_read_only: raw.is_read_only,
130 cache_type: raw.cache_type,
131 io_engine: raw.io_engine,
132 partuuid: raw.partuuid,
133 rate_limiter: raw.rate_limiter,
134 vhost_user_socket: socket,
135 })
136 }
137}
138
139#[derive(Debug, Clone, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct RawDrivePatch {
143 pub drive_id: String,
145 #[serde(default)]
147 pub path_on_host: Option<String>,
148 #[serde(default)]
150 pub rate_limiter: Option<serde_json::Value>,
151}
152
153#[derive(Debug, Clone, Serialize)]
155#[non_exhaustive]
156pub struct DrivePatch {
157 pub drive_id: DriveId,
159 pub path_on_host: Option<SafePath>,
161 pub rate_limiter: Option<serde_json::Value>,
163}
164
165impl TryFrom<RawDrivePatch> for DrivePatch {
166 type Error = String;
167
168 fn try_from(raw: RawDrivePatch) -> Result<Self, Self::Error> {
169 let drive_id = DriveId::new(raw.drive_id)?;
170 let path_on_host = match raw.path_on_host {
171 Some(p) => Some(SafePath::new(p).map_err(|e| format!("Invalid path_on_host: {e}"))?),
172 None => None,
173 };
174 Ok(Self {
175 drive_id,
176 path_on_host,
177 rate_limiter: raw.rate_limiter,
178 })
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 fn raw(id: &str) -> RawDriveConfig {
187 RawDriveConfig {
188 drive_id: id.into(),
189 path_on_host: "/tmp/img.bin".into(),
190 is_root_device: false,
191 is_read_only: false,
192 cache_type: CacheType::Unsafe,
193 io_engine: IoEngine::Sync,
194 partuuid: None,
195 rate_limiter: None,
196 socket: None,
197 }
198 }
199
200 #[test]
201 fn test_should_accept_minimal_drive() {
202 let cfg = DriveConfig::try_from(raw("rootfs")).unwrap();
203 assert_eq!(cfg.drive_id.as_str(), "rootfs");
204 }
205
206 #[test]
207 fn test_should_reject_invalid_drive_id() {
208 assert!(DriveConfig::try_from(raw("root-fs")).is_err());
209 }
210
211 #[test]
212 fn test_should_validate_partuuid() {
213 let mut r = raw("rootfs");
214 r.partuuid = Some("ABC123-def456".into());
215 assert!(DriveConfig::try_from(r).is_ok());
216 }
217
218 #[test]
219 fn test_should_reject_partuuid_with_special_chars() {
220 let mut r = raw("rootfs");
221 r.partuuid = Some("ABC*1".into());
222 assert!(DriveConfig::try_from(r).is_err());
223 }
224
225 #[test]
226 fn test_should_default_io_engine_to_sync() {
227 let json = r#"{"drive_id":"rootfs","path_on_host":"/tmp/img.bin"}"#;
228 let raw: RawDriveConfig = serde_json::from_str(json).unwrap();
229 assert_eq!(raw.io_engine, IoEngine::Sync);
230 }
231
232 #[test]
233 fn test_should_round_trip_drive_patch() {
234 let json = r#"{"drive_id":"rootfs","path_on_host":"/tmp/x.bin"}"#;
235 let raw: RawDrivePatch = serde_json::from_str(json).unwrap();
236 let p = DrivePatch::try_from(raw).unwrap();
237 assert_eq!(p.drive_id.as_str(), "rootfs");
238 assert!(p.path_on_host.is_some());
239 }
240}