squib_api/schemas/
pmem.rs1use serde::{Deserialize, Serialize};
4
5use super::common::{DriveId, SafePath};
6
7#[derive(Debug, Clone, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct RawPmemConfig {
11 pub pmem_id: String,
13 pub path_on_host: String,
15 #[serde(default)]
17 pub is_read_only: bool,
18 #[serde(default)]
20 pub is_root_device: bool,
21 #[serde(default)]
23 pub partuuid: Option<String>,
24 #[serde(default)]
26 pub rate_limiter: Option<serde_json::Value>,
27}
28
29#[derive(Debug, Clone, Serialize)]
31#[non_exhaustive]
32pub struct PmemConfig {
33 pub pmem_id: DriveId,
35 pub path_on_host: SafePath,
37 pub is_read_only: bool,
39 pub is_root_device: bool,
41 pub partuuid: Option<String>,
43 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#[derive(Debug, Clone, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct RawPmemPatch {
82 #[serde(alias = "id")]
86 pub pmem_id: String,
87 #[serde(default)]
89 pub rate_limiter: Option<serde_json::Value>,
90}
91
92#[derive(Debug, Clone, Serialize)]
94#[non_exhaustive]
95pub struct PmemPatch {
96 pub pmem_id: DriveId,
98 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}