libnvme/root.rs
1use std::io;
2use std::marker::PhantomData;
3use std::ptr::NonNull;
4
5#[cfg(has_hostid_from_file)]
6use libnvme_sys::nvmf_hostid_from_file;
7#[cfg(has_hostid_generate)]
8use libnvme_sys::nvmf_hostid_generate;
9use libnvme_sys::{
10 nvme_default_host, nvme_free_tree, nvme_lookup_host, nvme_root, nvme_scan,
11 nvmf_hostnqn_from_file, nvmf_hostnqn_generate,
12};
13
14use crate::host::{Host, Hosts};
15use crate::util::str_to_cstring;
16use crate::{Error, Result};
17
18/// The owning handle to the libnvme tree.
19///
20/// Created by [`Root::scan`], which calls `nvme_scan` to enumerate hosts,
21/// subsystems, controllers, and namespaces on the system. All other handle
22/// types ([`Host`], [`Subsystem`], [`Controller`], [`Namespace`]) borrow from
23/// the `Root` via the `'r` lifetime, so dropping the `Root` cascades-frees
24/// the entire tree via `nvme_free_tree`.
25///
26/// `Root` is `!Send` and `!Sync`. libnvme's scan state isn't documented as
27/// thread-safe; this restriction may be relaxed in a future release after
28/// an audit.
29///
30/// [`Host`]: crate::Host
31/// [`Subsystem`]: crate::Subsystem
32/// [`Controller`]: crate::Controller
33/// [`Namespace`]: crate::Namespace
34pub struct Root {
35 inner: NonNull<nvme_root>,
36 _not_send_sync: PhantomData<*const ()>,
37}
38
39impl Root {
40 /// Scan the system and build a libnvme handle tree.
41 ///
42 /// Equivalent to `nvme_scan(NULL)` — uses the default config-file lookup.
43 /// Returns the platform's last-set `errno` (via [`std::io::Error`]) if
44 /// libnvme returns NULL.
45 pub fn scan() -> Result<Self> {
46 // SAFETY: nvme_scan accepts NULL as a valid argument (means: use default
47 // config-file path). The returned pointer is either NULL or a fresh
48 // owned root handle we wrap below.
49 let raw = unsafe { nvme_scan(std::ptr::null()) };
50 let inner = NonNull::new(raw).ok_or_else(io::Error::last_os_error)?;
51 Ok(Root {
52 inner,
53 _not_send_sync: PhantomData,
54 })
55 }
56
57 /// Iterate over the hosts in this tree.
58 ///
59 /// libnvme typically reports a single host (the local machine) unless
60 /// the caller has configured Fabrics with multiple hostnqn entries.
61 pub fn hosts(&self) -> Hosts<'_> {
62 Hosts::new(self.inner.as_ptr())
63 }
64
65 /// Get or create the default host for this root.
66 ///
67 /// libnvme uses `/etc/nvme/hostnqn` and `/etc/nvme/hostid` if present,
68 /// otherwise generates new identifiers and persists them.
69 pub fn default_host(&self) -> Result<Host<'_>> {
70 // SAFETY: self.inner is a non-null nvme_root_t we own, alive for &self.
71 let raw = unsafe { nvme_default_host(self.inner.as_ptr()) };
72 if raw.is_null() {
73 return Err(Error::Os(io::Error::last_os_error()));
74 }
75 Ok(Host::from_raw(raw, self.inner.as_ptr()))
76 }
77
78 /// Look up or create a host with the given NQN and (optional) HostID.
79 ///
80 /// Use this when you need a non-default host identity — for instance, a
81 /// fabrics client that wants to present a specific HostNQN to a target.
82 pub fn lookup_host(&self, hostnqn: &str, hostid: Option<&str>) -> Result<Host<'_>> {
83 let hostnqn_c = str_to_cstring(hostnqn, "interior NUL byte in hostnqn")?;
84 let hostid_c = hostid
85 .map(|h| str_to_cstring(h, "interior NUL byte in hostid"))
86 .transpose()?;
87 let hostid_ptr = match &hostid_c {
88 Some(c) => c.as_ptr(),
89 None => std::ptr::null(),
90 };
91 // SAFETY: self.inner is a non-null nvme_root_t we own; hostnqn_c is a
92 // valid NUL-terminated C string alive for this call; hostid_ptr is
93 // either NULL or points to hostid_c which is alive through this call.
94 let raw = unsafe { nvme_lookup_host(self.inner.as_ptr(), hostnqn_c.as_ptr(), hostid_ptr) };
95 if raw.is_null() {
96 return Err(Error::Os(io::Error::last_os_error()));
97 }
98 Ok(Host::from_raw(raw, self.inner.as_ptr()))
99 }
100}
101
102/// Generate a fresh HostNQN (NVMe spec format, embeds a freshly-generated UUID).
103/// Equivalent to libnvme's `nvmf_hostnqn_generate`. Returned string is owned.
104pub fn generate_hostnqn() -> Result<String> {
105 // SAFETY: nvmf_hostnqn_generate takes no arguments and returns either NULL
106 // or a malloc-allocated C string we take ownership of via take_owned_cstr.
107 let raw = unsafe { nvmf_hostnqn_generate() };
108 take_owned_cstr(raw)
109}
110
111/// Generate a fresh HostID (random UUID, formatted as a hex string).
112///
113/// Only present when built against a libnvme that exposes
114/// `nvmf_hostid_generate` (added after libnvme 1.8).
115#[cfg(has_hostid_generate)]
116pub fn generate_hostid() -> Result<String> {
117 // SAFETY: nvmf_hostid_generate takes no arguments and returns either NULL
118 // or a malloc-allocated C string we take ownership of via take_owned_cstr.
119 let raw = unsafe { nvmf_hostid_generate() };
120 take_owned_cstr(raw)
121}
122
123/// Read the local HostNQN from `/etc/nvme/hostnqn` if it exists.
124pub fn hostnqn_from_file() -> Result<String> {
125 // SAFETY: nvmf_hostnqn_from_file takes no arguments and returns either NULL
126 // or a malloc-allocated C string we take ownership of via take_owned_cstr.
127 let raw = unsafe { nvmf_hostnqn_from_file() };
128 take_owned_cstr(raw)
129}
130
131/// Read the local HostID from `/etc/nvme/hostid` if it exists.
132///
133/// Only present when built against a libnvme that exposes
134/// `nvmf_hostid_from_file` (added after libnvme 1.8).
135#[cfg(has_hostid_from_file)]
136pub fn hostid_from_file() -> Result<String> {
137 // SAFETY: nvmf_hostid_from_file takes no arguments and returns either NULL
138 // or a malloc-allocated C string we take ownership of via take_owned_cstr.
139 let raw = unsafe { nvmf_hostid_from_file() };
140 take_owned_cstr(raw)
141}
142
143/// Take ownership of a libnvme-allocated `*mut c_char`, copy it into an owned
144/// `String`, and free the original via libc's `free`.
145///
146/// The free runs unconditionally on the non-null path — including the
147/// UTF-8 error case — so libnvme's allocation is never leaked, even on
148/// bad input.
149fn take_owned_cstr(ptr: *mut std::os::raw::c_char) -> Result<String> {
150 if ptr.is_null() {
151 return Err(Error::NotAvailable);
152 }
153 // Copy bytes out before freeing so the free is unconditional regardless
154 // of whether the bytes form valid UTF-8.
155 // SAFETY: ptr is non-null (checked above) and points to a valid
156 // NUL-terminated C string produced by libnvme via malloc. The CStr
157 // borrow ends before we free the pointer.
158 let bytes = unsafe { std::ffi::CStr::from_ptr(ptr) }.to_bytes().to_vec();
159 // SAFETY: ptr came from a libnvme allocator (malloc-family). We own it
160 // exclusively; nothing aliases it past this point.
161 unsafe { libc_free(ptr as *mut _) };
162 String::from_utf8(bytes).map_err(|e| Error::Utf8(e.utf8_error()))
163}
164
165unsafe extern "C" {
166 #[link_name = "free"]
167 fn libc_free(ptr: *mut std::ffi::c_void);
168}
169
170impl Drop for Root {
171 fn drop(&mut self) {
172 // SAFETY: we own self.inner and this is the only Drop path; libnvme's
173 // tree-free recursively releases all child handles.
174 unsafe { nvme_free_tree(self.inner.as_ptr()) };
175 }
176}
177
178impl std::fmt::Debug for Root {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.debug_struct("Root")
181 .field("inner", &self.inner.as_ptr())
182 .finish()
183 }
184}