Skip to main content

nix_bindings/
error.rs

1//! Error types and FFI error-handling helpers.
2//!
3//! Re-exported at the crate root as [`crate::Error`] and [`crate::Result`].
4
5use std::{ffi::CStr, fmt};
6
7use crate::sys;
8
9/// Result type for Nix operations.
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// Error types for Nix operations.
13#[derive(Debug)]
14pub enum Error {
15  /// Unknown error from Nix C API.
16  Unknown(String),
17
18  /// Overflow error.
19  Overflow,
20
21  /// Key not found error.
22  KeyNotFound(String),
23
24  /// List index out of bounds.
25  IndexOutOfBounds {
26    /// The index that was requested.
27    index:  usize,
28    /// The actual length of the list.
29    length: usize,
30  },
31
32  /// Nix evaluation error.
33  EvalError(String),
34
35  /// Invalid value type conversion.
36  InvalidType {
37    /// Expected type.
38    expected: &'static str,
39    /// Actual type.
40    actual:   String,
41  },
42  /// Null pointer error.
43  NullPointer,
44
45  /// String conversion error.
46  StringConversion(std::ffi::NulError),
47}
48
49impl fmt::Display for Error {
50  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51    match self {
52      Error::Unknown(msg) => write!(f, "Unknown error: {msg}"),
53      Error::Overflow => write!(f, "Overflow error"),
54      Error::KeyNotFound(key) => write!(f, "Key not found: {key}"),
55      Error::IndexOutOfBounds { index, length } => {
56        write!(f, "Index out of bounds: index {index}, length {length}")
57      },
58      Error::EvalError(msg) => write!(f, "Evaluation error: {msg}"),
59      Error::InvalidType { expected, actual } => {
60        write!(f, "Invalid type: expected {expected}, got {actual}")
61      },
62      Error::NullPointer => write!(f, "Null pointer error"),
63      Error::StringConversion(e) => write!(f, "String conversion error: {e}"),
64    }
65  }
66}
67
68impl std::error::Error for Error {}
69
70impl From<std::ffi::NulError> for Error {
71  fn from(e: std::ffi::NulError) -> Self {
72    Error::StringConversion(e)
73  }
74}
75
76/// Extract a string from a Nix context using a callback-based API.
77///
78/// Many Nix C API functions return strings via callbacks. This helper
79/// makes that pattern ergonomic.
80///
81/// # Safety
82///
83/// `call` must invoke `callback` with a valid string pointer and length.
84#[cfg(feature = "store")]
85pub(crate) unsafe fn string_from_callback<F>(call: F) -> Option<String>
86where
87  F: FnOnce(sys::nix_get_string_callback, *mut std::os::raw::c_void),
88{
89  let mut result: Option<String> = None;
90  let user_data = &mut result as *mut _ as *mut std::os::raw::c_void;
91  call(Some(collect_string), user_data);
92  result
93}
94
95/// Extract a string from a fallible Nix callback API.
96///
97/// # Safety
98///
99/// `call` must invoke `callback` with a valid string pointer and length when
100/// it returns [`sys::nix_err_NIX_OK`].
101#[cfg(feature = "store")]
102pub(crate) unsafe fn checked_string_from_callback<F>(
103  ctx: *mut sys::nix_c_context,
104  call: F,
105) -> Result<String>
106where
107  F: FnOnce(
108    sys::nix_get_string_callback,
109    *mut std::os::raw::c_void,
110  ) -> sys::nix_err,
111{
112  let mut result = None;
113  let user_data = &mut result as *mut _ as *mut std::os::raw::c_void;
114  let err = call(Some(collect_string), user_data);
115  check_err(ctx, err)?;
116  result.ok_or_else(|| {
117    Error::Unknown("Nix string callback returned no string".to_string())
118  })
119}
120
121#[cfg(feature = "store")]
122unsafe extern "C" fn collect_string(
123  start: *const std::os::raw::c_char,
124  n: std::os::raw::c_uint,
125  user_data: *mut std::os::raw::c_void,
126) {
127  let result = unsafe { &mut *(user_data as *mut Option<String>) };
128  if !start.is_null() {
129    let bytes =
130      unsafe { std::slice::from_raw_parts(start.cast::<u8>(), n as usize) };
131    *result = std::str::from_utf8(bytes).ok().map(str::to_owned);
132  }
133}
134
135/// Check a Nix error code and convert to `Result`, extracting the real
136/// error message from the context.
137#[cfg(feature = "store")]
138pub(crate) fn check_err(
139  ctx: *mut sys::nix_c_context,
140  err: sys::nix_err,
141) -> Result<()> {
142  if err == sys::nix_err_NIX_OK {
143    return Ok(());
144  }
145
146  // Extract the real error message from the context.
147  // nix_err_msg returns a borrowed pointer valid until the next Nix call.
148  // We must copy it to a String immediately.
149  let msg = unsafe {
150    let ptr = sys::nix_err_msg(std::ptr::null_mut(), ctx, std::ptr::null_mut());
151    if ptr.is_null() {
152      None
153    } else {
154      Some(CStr::from_ptr(ptr).to_string_lossy().into_owned())
155    }
156  };
157
158  // For NIX_ERR_NIX_ERROR, also try to get the richer info message.
159  let detail = if err == sys::nix_err_NIX_ERR_NIX_ERROR {
160    unsafe {
161      string_from_callback(|cb, ud| {
162        sys::nix_err_info_msg(std::ptr::null_mut(), ctx, cb, ud);
163      })
164    }
165  } else {
166    None
167  };
168
169  // Decorate with the symbolic error name (e.g. "NixError", "Key",
170  // "Overflow") when the API exposes one for this code. Improves
171  // diagnostics when the message itself is empty or generic.
172  let name = unsafe {
173    string_from_callback(|cb, ud| {
174      sys::nix_err_name(std::ptr::null_mut(), ctx, cb, ud);
175    })
176  };
177
178  let base_message = detail
179    .or(msg)
180    .unwrap_or_else(|| format!("Nix error code: {err}"));
181  let message = match name {
182    Some(n) if !n.is_empty() => format!("[{n}] {base_message}"),
183    _ => base_message,
184  };
185
186  match err {
187    sys::nix_err_NIX_ERR_UNKNOWN => Err(Error::Unknown(message)),
188    sys::nix_err_NIX_ERR_OVERFLOW => Err(Error::Overflow),
189    sys::nix_err_NIX_ERR_KEY => Err(Error::KeyNotFound(message)),
190    sys::nix_err_NIX_ERR_NIX_ERROR => Err(Error::EvalError(message)),
191    _ => Err(Error::Unknown(message)),
192  }
193}
194
195/// Convert a possibly-null pointer from a Nix C API call into a [`NonNull`],
196/// surfacing the context's parked error when the pointer is null.
197///
198/// Nix's pointer-returning entry points (e.g. `nix_flake_lock`) report failure
199/// by returning null and leaving the real error (code + message) on the
200/// context. Mapping null straight to [`Error::NullPointer`] discards that
201/// message, which is how a genuine Nix failure ("cannot find revision ...",
202/// "access to URI ... is blocked", ...) is reduced to an opaque "Null pointer
203/// error". This reads the parked error via [`check_err`] instead.
204///
205/// # Errors
206///
207/// Returns the parked context error when one is set, or [`Error::NullPointer`]
208/// when the pointer is null with no error recorded (a genuinely value-less
209/// null, such as an absent optional).
210#[cfg(feature = "store")]
211pub(crate) fn check_ptr<T>(
212  ctx: *mut sys::nix_c_context,
213  ptr: *mut T,
214) -> Result<std::ptr::NonNull<T>> {
215  if let Some(non_null) = std::ptr::NonNull::new(ptr) {
216    return Ok(non_null);
217  }
218  // SAFETY: `ctx` is a live context pointer owned by the caller.
219  let code = unsafe { sys::nix_err_code(ctx) };
220  match check_err(ctx, code) {
221    Ok(()) => Err(Error::NullPointer),
222    Err(e) => Err(e),
223  }
224}