Skip to main content

squib_api/schemas/
pmem.rs

1//! `/pmem/{id}` PUT and PATCH bodies — virtio-pmem on a memory-mapped file.
2
3use serde::{Deserialize, Serialize};
4
5use super::common::{DriveId, SafePath};
6
7/// Raw `/pmem/{id}` PUT body off the wire.
8#[derive(Debug, Clone, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct RawPmemConfig {
11    /// Identifier (we re-use the drive identifier rules).
12    pub pmem_id: String,
13    /// Backing-file path.
14    pub path_on_host: String,
15    /// Whether the device is exposed read-only.
16    #[serde(default)]
17    pub is_read_only: bool,
18    /// Whether the guest mounts this as the root device (replaces virtio-block root).
19    #[serde(default)]
20    pub is_root_device: bool,
21    /// Optional partition UUID for `root=PARTUUID=...`.
22    #[serde(default)]
23    pub partuuid: Option<String>,
24    /// Optional rate limiter passthrough.
25    #[serde(default)]
26    pub rate_limiter: Option<serde_json::Value>,
27}
28
29/// Validated `/pmem/{id}` PUT body.
30#[derive(Debug, Clone, Serialize)]
31#[non_exhaustive]
32pub struct PmemConfig {
33    /// Validated identifier.
34    pub pmem_id: DriveId,
35    /// Validated host path.
36    pub path_on_host: SafePath,
37    /// Read-only flag.
38    pub is_read_only: bool,
39    /// Root-device flag.
40    pub is_root_device: bool,
41    /// Validated partition UUID.
42    pub partuuid: Option<String>,
43    /// Optional rate limiter passthrough.
44    pub rate_limiter: Option<serde_json::Value>,
45}
46
47fn validate_partuuid(uuid: &str) -> Result<(), String> {
48    if uuid.is_empty() || uuid.len() > 64 {
49        return Err("Invalid partuuid: must be 1..=64 bytes".into());
50    }
51    if !uuid.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') {
52        return Err("Invalid partuuid: only [A-Za-z0-9-] permitted".into());
53    }
54    Ok(())
55}
56
57impl TryFrom<RawPmemConfig> for PmemConfig {
58    type Error = String;
59
60    fn try_from(raw: RawPmemConfig) -> Result<Self, Self::Error> {
61        let pmem_id = DriveId::new(raw.pmem_id)?;
62        let path_on_host =
63            SafePath::new(raw.path_on_host).map_err(|e| format!("Invalid path_on_host: {e}"))?;
64        if let Some(p) = raw.partuuid.as_deref() {
65            validate_partuuid(p)?;
66        }
67        Ok(Self {
68            pmem_id,
69            path_on_host,
70            is_read_only: raw.is_read_only,
71            is_root_device: raw.is_root_device,
72            partuuid: raw.partuuid,
73            rate_limiter: raw.rate_limiter,
74        })
75    }
76}
77
78/// Raw `/pmem/{id}` PATCH body. Mirrors upstream's `PmemDeviceUpdateConfig`.
79#[derive(Debug, Clone, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct RawPmemPatch {
82    /// Pmem device ID being patched (must match the URL `{id}`). Upstream calls this
83    /// `id`; we accept both `pmem_id` and `id` for forward compat with squib's other
84    /// patch bodies.
85    #[serde(alias = "id")]
86    pub pmem_id: String,
87    /// Replacement rate limiter.
88    #[serde(default)]
89    pub rate_limiter: Option<serde_json::Value>,
90}
91
92/// Validated `/pmem/{id}` PATCH body.
93#[derive(Debug, Clone, Serialize)]
94#[non_exhaustive]
95pub struct PmemPatch {
96    /// Validated pmem device ID.
97    pub pmem_id: DriveId,
98    /// Replacement rate limiter passthrough.
99    pub rate_limiter: Option<serde_json::Value>,
100}
101
102impl TryFrom<RawPmemPatch> for PmemPatch {
103    type Error = String;
104
105    fn try_from(raw: RawPmemPatch) -> Result<Self, Self::Error> {
106        Ok(Self {
107            pmem_id: DriveId::new(raw.pmem_id)?,
108            rate_limiter: raw.rate_limiter,
109        })
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_should_accept_minimal_pmem() {
119        let cfg = PmemConfig::try_from(RawPmemConfig {
120            pmem_id: "pmem0".into(),
121            path_on_host: "/tmp/p.bin".into(),
122            is_read_only: false,
123            is_root_device: false,
124            partuuid: None,
125            rate_limiter: None,
126        })
127        .unwrap();
128        assert_eq!(cfg.pmem_id.as_str(), "pmem0");
129    }
130
131    #[test]
132    fn test_should_reject_invalid_pmem_id() {
133        assert!(
134            PmemConfig::try_from(RawPmemConfig {
135                pmem_id: "pmem-0".into(),
136                path_on_host: "/tmp/p.bin".into(),
137                is_read_only: false,
138                is_root_device: false,
139                partuuid: None,
140                rate_limiter: None,
141            })
142            .is_err()
143        );
144    }
145
146    #[test]
147    fn test_should_round_trip_pmem_patch() {
148        let raw: RawPmemPatch = serde_json::from_str(r#"{"pmem_id":"pmem0"}"#).unwrap();
149        let patch = PmemPatch::try_from(raw).unwrap();
150        assert_eq!(patch.pmem_id.as_str(), "pmem0");
151    }
152
153    #[test]
154    fn test_should_accept_id_alias_in_pmem_patch() {
155        let raw: RawPmemPatch = serde_json::from_str(r#"{"id":"pmem0"}"#).unwrap();
156        let patch = PmemPatch::try_from(raw).unwrap();
157        assert_eq!(patch.pmem_id.as_str(), "pmem0");
158    }
159}