use windows::core::HRESULT;
pub type HnsResult<T> = Result<T, HnsError>;
#[derive(Debug, thiserror::Error)]
pub enum HnsError {
#[error("HCN resource not found: {id}")]
NotFound {
id: String,
},
#[error("access denied (caller must be Administrator or Hyper-V Administrator): HRESULT 0x{hresult:08x}")]
AccessDenied {
hresult: i32,
},
#[error("HCN rejected JSON document: {message} (HRESULT 0x{hresult:08x})")]
InvalidJson {
hresult: i32,
message: String,
},
#[error("HCN NAT subnet conflict: {subnet}")]
SubnetConflict {
subnet: String,
},
#[error("HCN policy rejected: {message} (HRESULT 0x{hresult:08x})")]
PolicyInvalid {
hresult: i32,
message: String,
},
#[error("HCN call failed: {message} (HRESULT 0x{hresult:08x})")]
Other {
hresult: i32,
message: String,
},
#[error("HCN JSON error: {0}")]
Json(#[from] serde_json::Error),
}
impl HnsError {
#[must_use]
pub fn from_hresult(hr: HRESULT, message: impl Into<String>) -> Self {
match hr.0 {
-0x7FC4_FFFF | -0x7FC4_FFFC | -0x7FC4_FFF4 => Self::NotFound { id: message.into() },
-0x7FFF_FFFB => Self::AccessDenied { hresult: hr.0 },
-0x7FC4_FFF3 => Self::InvalidJson {
hresult: hr.0,
message: message.into(),
},
-0x7FC4_FFEC => Self::SubnetConflict {
subnet: message.into(),
},
-0x7FC4_FFF2 => Self::PolicyInvalid {
hresult: hr.0,
message: message.into(),
},
_ => Self::Other {
hresult: hr.0,
message: message.into(),
},
}
}
pub fn check(hr: HRESULT, message: impl FnOnce() -> String) -> HnsResult<()> {
if hr.is_ok() {
Ok(())
} else {
Err(Self::from_hresult(hr, message()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use windows::core::HRESULT;
fn hr(code: i32) -> HRESULT {
HRESULT(code)
}
#[test]
fn access_denied_maps_specifically() {
let err = HnsError::from_hresult(hr(-0x7FFF_FFFB), "raw");
assert!(matches!(err, HnsError::AccessDenied { .. }));
}
#[test]
fn network_not_found_maps_specifically() {
let err = HnsError::from_hresult(hr(-0x7FC4_FFFF), "my-net");
if let HnsError::NotFound { id } = err {
assert_eq!(id, "my-net");
} else {
panic!("expected NotFound, got {err:?}");
}
}
#[test]
fn subnet_conflict_maps_specifically() {
let err = HnsError::from_hresult(hr(-0x7FC4_FFEC), "10.88.0.0/16");
if let HnsError::SubnetConflict { subnet } = err {
assert_eq!(subnet, "10.88.0.0/16");
} else {
panic!("expected SubnetConflict, got {err:?}");
}
}
#[test]
fn unknown_hresult_falls_through_to_other() {
let err = HnsError::from_hresult(hr(-0x1234_5678), "unknown code");
if let HnsError::Other { hresult, message } = err {
assert_eq!(hresult, -0x1234_5678);
assert_eq!(message, "unknown code");
} else {
panic!("expected Other, got {err:?}");
}
}
#[test]
fn check_success_returns_ok() {
let r = HnsError::check(HRESULT(0), || "never called".to_string());
assert!(r.is_ok());
}
#[test]
fn check_failure_returns_err() {
let r = HnsError::check(hr(-0x7FFF_FFFB), || "access denied".to_string());
assert!(matches!(r, Err(HnsError::AccessDenied { .. })));
}
}