#![allow(clippy::missing_errors_doc)]
use core::ffi::c_void;
use windows::core::{GUID, HSTRING, PWSTR};
use windows::Win32::System::HostComputeNetwork::{
HcnCreateNamespace, HcnDeleteNamespace, HcnEnumerateNamespaces, HcnModifyNamespace,
HcnOpenNamespace, HcnQueryNamespaceProperties,
};
use crate::error::{HnsError, HnsResult};
use crate::handle::{HcnNamespaceHandle, OwnedNamespace};
use crate::schema::{
HostComputeNamespace, ModifyNamespaceSettingRequest, ModifyRequestType, NamespaceType,
SchemaVersion,
};
#[derive(Debug)]
pub struct Namespace {
id: GUID,
handle: OwnedNamespace,
}
impl Namespace {
pub fn create_host_default() -> HnsResult<Self> {
let id = GUID::new().map_err(|e| HnsError::Other {
hresult: e.code().0,
message: format!("GUID::new failed: {e}"),
})?;
let spec = HostComputeNamespace {
ty: NamespaceType::HostDefault,
schema_version: SchemaVersion::default(),
..HostComputeNamespace::default()
};
Self::create(id, &spec)
}
pub fn create(id: GUID, spec: &HostComputeNamespace) -> HnsResult<Self> {
let settings_json = serde_json::to_string(spec)?;
let settings_hstring = HSTRING::from(settings_json);
let mut raw: *mut c_void = core::ptr::null_mut();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnCreateNamespace(&id, &settings_hstring, &mut raw, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnCreateNamespace"))?;
}
if raw.is_null() {
return Err(HnsError::Other {
hresult: 0,
message: "HcnCreateNamespace returned null handle".to_string(),
});
}
let handle = unsafe { OwnedNamespace::from_raw(raw as HcnNamespaceHandle) };
Ok(Self { id, handle })
}
pub fn open(id: GUID) -> HnsResult<Self> {
let mut raw: *mut c_void = core::ptr::null_mut();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnOpenNamespace(&id, &mut raw, Some(&mut err_record)).map_err(|e| {
classify_error(e.code(), err_record, format!("HcnOpenNamespace({id:?})"))
})?;
}
if raw.is_null() {
return Err(HnsError::NotFound {
id: format!("{id:?}"),
});
}
let handle = unsafe { OwnedNamespace::from_raw(raw as HcnNamespaceHandle) };
Ok(Self { id, handle })
}
pub fn delete(id: GUID) -> HnsResult<()> {
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnDeleteNamespace(&id, Some(&mut err_record)).map_err(|e| {
classify_error(e.code(), err_record, format!("HcnDeleteNamespace({id:?})"))
})?;
}
Ok(())
}
pub fn add_endpoint(&self, endpoint_id: GUID) -> HnsResult<()> {
let req = ModifyNamespaceSettingRequest {
resource_type: 1, request_type: ModifyRequestType::Add,
settings: serde_json::json!({ "EndpointId": format!("{endpoint_id:?}") }),
};
self.modify_json(&serde_json::to_string(&req)?)
}
pub fn remove_endpoint(&self, endpoint_id: GUID) -> HnsResult<()> {
let req = ModifyNamespaceSettingRequest {
resource_type: 1, request_type: ModifyRequestType::Remove,
settings: serde_json::json!({ "EndpointId": format!("{endpoint_id:?}") }),
};
self.modify_json(&serde_json::to_string(&req)?)
}
pub fn modify_json(&self, modification_json: &str) -> HnsResult<()> {
let mod_hstring = HSTRING::from(modification_json);
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnModifyNamespace(self.handle.as_raw(), &mod_hstring, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnModifyNamespace"))?;
}
Ok(())
}
pub fn query(&self, query_json: &str) -> HnsResult<HostComputeNamespace> {
let query_hstring = HSTRING::from(query_json);
let mut out_properties: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnQueryNamespaceProperties(
self.handle.as_raw(),
&query_hstring,
&mut out_properties,
Some(&mut err_record),
)
.map_err(|e| classify_error(e.code(), err_record, "HcnQueryNamespaceProperties"))?;
}
let json = decode_pwstr(out_properties);
let parsed: HostComputeNamespace = serde_json::from_str(&json)?;
Ok(parsed)
}
pub fn list_endpoints(&self) -> HnsResult<Vec<String>> {
let props = self.query("{}")?;
Ok(props
.resources
.iter()
.filter(|r| r.ty == "Endpoint")
.map(|r| r.id.clone())
.collect())
}
#[must_use]
pub fn id(&self) -> GUID {
self.id
}
#[must_use]
pub fn handle(&self) -> &OwnedNamespace {
&self.handle
}
}
pub fn list(query_json: &str) -> HnsResult<Vec<GUID>> {
let query_hstring = HSTRING::from(query_json);
let mut out_namespaces: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnEnumerateNamespaces(&query_hstring, &mut out_namespaces, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnEnumerateNamespaces"))?;
}
let json = decode_pwstr(out_namespaces);
if json.is_empty() {
return Ok(Vec::new());
}
let arr: Vec<String> = serde_json::from_str(&json).unwrap_or_default();
let mut guids = Vec::with_capacity(arr.len());
for s in arr {
let bare = s.trim_matches(|c: char| c == '{' || c == '}');
let guid = GUID::try_from(bare).map_err(|e| HnsError::Other {
hresult: 0,
message: format!("bad GUID from HcnEnumerateNamespaces: {s}: {e}"),
})?;
guids.push(guid);
}
Ok(guids)
}
fn decode_pwstr(p: PWSTR) -> String {
use windows::Win32::Foundation::{LocalFree, HLOCAL};
if p.is_null() {
return String::new();
}
let s = unsafe { p.to_string().unwrap_or_default() };
unsafe {
let _ = LocalFree(Some(HLOCAL(p.0.cast())));
}
s
}
fn classify_error<S: Into<String>>(
hr: windows::core::HRESULT,
err_record: PWSTR,
context: S,
) -> HnsError {
let ctx: String = context.into();
let decoded = decode_pwstr(err_record);
let msg = if decoded.is_empty() {
ctx
} else {
format!("{ctx}: {decoded}")
};
HnsError::from_hresult(hr, msg)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::ModifyRequestType;
#[test]
fn decode_pwstr_null_returns_empty() {
let s = decode_pwstr(PWSTR::null());
assert!(s.is_empty());
}
#[test]
fn classify_error_access_denied_hresult() {
let err = classify_error(windows::core::HRESULT(-0x7FFF_FFFB), PWSTR::null(), "ctx");
assert!(matches!(err, HnsError::AccessDenied { .. }));
}
#[test]
fn classify_error_preserves_context_when_errrecord_empty() {
let err = classify_error(
windows::core::HRESULT(-0x1234_5678),
PWSTR::null(),
"HcnCreateNamespace",
);
if let HnsError::Other { message, .. } = err {
assert_eq!(message, "HcnCreateNamespace");
} else {
panic!("expected Other, got {err:?}");
}
}
#[test]
fn add_endpoint_request_serialises_with_resource_type_1() {
let endpoint_id = GUID::from_u128(0x1234_5678_9abc_def0_1122_3344_5566_7788);
let req = ModifyNamespaceSettingRequest {
resource_type: 1,
request_type: ModifyRequestType::Add,
settings: serde_json::json!({
"EndpointId": format!("{endpoint_id:?}")
}),
};
let v: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(v["ResourceType"], serde_json::json!(1));
assert_eq!(v["RequestType"], serde_json::json!("Add"));
assert!(
v["Settings"]["EndpointId"]
.as_str()
.unwrap()
.contains("12345678"),
"EndpointId should contain GUID data: {v}"
);
}
#[test]
fn remove_endpoint_request_uses_remove_verb() {
let endpoint_id = GUID::zeroed();
let req = ModifyNamespaceSettingRequest {
resource_type: 1,
request_type: ModifyRequestType::Remove,
settings: serde_json::json!({
"EndpointId": format!("{endpoint_id:?}")
}),
};
let v: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(v["RequestType"], serde_json::json!("Remove"));
assert_eq!(v["ResourceType"], serde_json::json!(1));
}
}