Skip to main content

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}