use serde::{Deserialize, Serialize};
use super::common::{DriveId, SafePath};
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawPmemConfig {
pub pmem_id: String,
pub path_on_host: String,
#[serde(default)]
pub is_read_only: bool,
#[serde(default)]
pub is_root_device: bool,
#[serde(default)]
pub partuuid: Option<String>,
#[serde(default)]
pub rate_limiter: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct PmemConfig {
pub pmem_id: DriveId,
pub path_on_host: SafePath,
pub is_read_only: bool,
pub is_root_device: bool,
pub partuuid: Option<String>,
pub rate_limiter: Option<serde_json::Value>,
}
fn validate_partuuid(uuid: &str) -> Result<(), String> {
if uuid.is_empty() || uuid.len() > 64 {
return Err("Invalid partuuid: must be 1..=64 bytes".into());
}
if !uuid.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') {
return Err("Invalid partuuid: only [A-Za-z0-9-] permitted".into());
}
Ok(())
}
impl TryFrom<RawPmemConfig> for PmemConfig {
type Error = String;
fn try_from(raw: RawPmemConfig) -> Result<Self, Self::Error> {
let pmem_id = DriveId::new(raw.pmem_id)?;
let path_on_host =
SafePath::new(raw.path_on_host).map_err(|e| format!("Invalid path_on_host: {e}"))?;
if let Some(p) = raw.partuuid.as_deref() {
validate_partuuid(p)?;
}
Ok(Self {
pmem_id,
path_on_host,
is_read_only: raw.is_read_only,
is_root_device: raw.is_root_device,
partuuid: raw.partuuid,
rate_limiter: raw.rate_limiter,
})
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawPmemPatch {
#[serde(alias = "id")]
pub pmem_id: String,
#[serde(default)]
pub rate_limiter: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct PmemPatch {
pub pmem_id: DriveId,
pub rate_limiter: Option<serde_json::Value>,
}
impl TryFrom<RawPmemPatch> for PmemPatch {
type Error = String;
fn try_from(raw: RawPmemPatch) -> Result<Self, Self::Error> {
Ok(Self {
pmem_id: DriveId::new(raw.pmem_id)?,
rate_limiter: raw.rate_limiter,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_accept_minimal_pmem() {
let cfg = PmemConfig::try_from(RawPmemConfig {
pmem_id: "pmem0".into(),
path_on_host: "/tmp/p.bin".into(),
is_read_only: false,
is_root_device: false,
partuuid: None,
rate_limiter: None,
})
.unwrap();
assert_eq!(cfg.pmem_id.as_str(), "pmem0");
}
#[test]
fn test_should_reject_invalid_pmem_id() {
assert!(
PmemConfig::try_from(RawPmemConfig {
pmem_id: "pmem-0".into(),
path_on_host: "/tmp/p.bin".into(),
is_read_only: false,
is_root_device: false,
partuuid: None,
rate_limiter: None,
})
.is_err()
);
}
#[test]
fn test_should_round_trip_pmem_patch() {
let raw: RawPmemPatch = serde_json::from_str(r#"{"pmem_id":"pmem0"}"#).unwrap();
let patch = PmemPatch::try_from(raw).unwrap();
assert_eq!(patch.pmem_id.as_str(), "pmem0");
}
#[test]
fn test_should_accept_id_alias_in_pmem_patch() {
let raw: RawPmemPatch = serde_json::from_str(r#"{"id":"pmem0"}"#).unwrap();
let patch = PmemPatch::try_from(raw).unwrap();
assert_eq!(patch.pmem_id.as_str(), "pmem0");
}
}