diff --git a/Cargo.lock b/Cargo.lock index 1cd2461..5346be7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,7 @@ name = "nixide" version = "0.1.0" dependencies = [ "nixide-sys", + "serial_test", ] [[package]] diff --git a/nixide/Cargo.toml b/nixide/Cargo.toml index 4c640de..fc0fa79 100644 --- a/nixide/Cargo.toml +++ b/nixide/Cargo.toml @@ -7,7 +7,7 @@ license = "GPL-3.0" repository = "https://codeberg.org/luminary/nixide" authors = [ "_cry64 ", - "foxxyora " + "foxxyora ", ] edition = "2024" @@ -25,3 +25,6 @@ gc = [] [dependencies] nixide-sys = { path = "../nixide-sys", version = "0.1.0" } + +[dev-dependencies] +serial_test = "3.4.0" diff --git a/nixide/src/context.rs b/nixide/src/context.rs new file mode 100644 index 0000000..189a246 --- /dev/null +++ b/nixide/src/context.rs @@ -0,0 +1,71 @@ +use std::ptr::NonNull; + +use crate::error::NixError; +use nixide_sys as sys; + +/// Nix context for managing library state. +/// +/// This is the root object for all Nix operations. It manages the lifetime +/// of the Nix C API context and provides automatic cleanup. +pub struct Context { + inner: NonNull, +} + +impl Context { + /// Create a new Nix context. + /// + /// This initializes the Nix C API context and the required libraries. + /// + /// # Errors + /// + /// Returns an error if context creation or library initialization fails. + pub fn new() -> Result { + // SAFETY: nix_c_context_create is safe to call + let ctx_ptr = unsafe { sys::nix_c_context_create() }; + let ctx = Context { + inner: NonNull::new(ctx_ptr).ok_or(NixError::NullPtr { + location: "nix_c_context_create", + })?, + }; + + // Initialize required libraries + unsafe { + NixError::from( + sys::nix_libutil_init(ctx.inner.as_ptr()), + "nix_libutil_init", + )?; + NixError::from( + sys::nix_libstore_init(ctx.inner.as_ptr()), + "nix_libstore_init", + )?; + NixError::from( + sys::nix_libexpr_init(ctx.inner.as_ptr()), + "nix_libexpr_init", + )?; + }; + + Ok(ctx) + } + + /// Get the raw context pointer. + /// + /// # Safety + /// + /// The caller must ensure the pointer is used safely. + pub unsafe fn as_ptr(&self) -> *mut sys::nix_c_context { + self.inner.as_ptr() + } +} + +impl Drop for Context { + fn drop(&mut self) { + // SAFETY: We own the context and it's valid until drop + unsafe { + sys::nix_c_context_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: Context can be shared between threads +unsafe impl Send for Context {} +unsafe impl Sync for Context {} diff --git a/nixide/src/error.rs b/nixide/src/error.rs new file mode 100644 index 0000000..0e2de14 --- /dev/null +++ b/nixide/src/error.rs @@ -0,0 +1,183 @@ +use std::ffi::NulError; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::ptr::NonNull; + +use crate::sys; + +/// Standard (nix_err) and some additional error codes +/// produced by the libnix C API. +#[derive(Debug)] +pub enum NixError { + /// A generic Nix error occurred. + /// + /// # Reason + /// + /// This error code is returned when a generic Nix error occurred during the + /// function execution. + NixError { location: &'static str }, + + /// An overflow error occurred. + /// + /// # Reason + /// + /// This error code is returned when an overflow error occurred during the + /// function execution. + Overflow { location: &'static str }, + + /// A key/index access error occurred in C API functions. + /// + /// # Reason + /// + /// This error code is returned when accessing a key, index, or identifier that + /// does not exist in C API functions. Common scenarios include: + /// - Setting keys that don't exist (nix_setting_get, nix_setting_set) + /// - List indices that are out of bounds (nix_get_list_byidx*) + /// - Attribute names that don't exist (nix_get_attr_byname*) + /// - Attribute indices that are out of bounds (nix_get_attr_byidx*, nix_get_attr_name_byidx) + /// + /// This error typically indicates incorrect usage or assumptions about data structure + /// contents, rather than internal Nix evaluation errors. + /// + /// # Note + /// + /// This error code should ONLY be returned by C API functions themselves, + /// not by underlying Nix evaluation. For example, evaluating `{}.foo` in Nix + /// will throw a normal error (NIX_ERR_NIX_ERROR), not NIX_ERR_KEY. + KeyNotFound { + location: &'static str, + key: Option, + }, + + /// An unknown error occurred. + /// + /// # Reason + /// + /// This error code is returned when an unknown error occurred during the + /// function execution. + Unknown { + location: &'static str, + reason: String, + }, + + /// An undocumented error occurred. + /// + /// # Reason + /// + /// The libnix C API defines `enum nix_err` as a signed integer value. + /// In the (unexpected) event libnix returns an error code with an + /// invalid enum value, or one I new addition I didn't know existed, + /// then an [NixError::Undocumented] is considered to have occurred. + Undocumented { + location: &'static str, + err_code: sys::nix_err, + }, + + ////////////////////// + // NON-STANDARD ERRORS + ////////////////////// + /// NulError + NulError { location: &'static str }, + + /// Non-standard + NullPtr { location: &'static str }, + + /// Invalid Argument + InvalidArg { + location: &'static str, + reason: &'static str, // XXX: TODO: make this a String + }, + + /// Invalid Type + InvalidType { + location: &'static str, + expected: &'static str, + got: String, + }, +} + +impl NixError { + pub fn from(err_code: sys::nix_err, location: &'static str) -> Result<(), NixError> { + #[allow(nonstandard_style)] + match err_code { + sys::nix_err_NIX_OK => Ok(()), + + sys::nix_err_NIX_ERR_OVERFLOW => Err(NixError::Overflow { location }), + sys::nix_err_NIX_ERR_KEY => Err(NixError::KeyNotFound { + location, + key: None, + }), + sys::nix_err_NIX_ERR_NIX_ERROR => Err(NixError::NixError { location }), + + sys::nix_err_NIX_ERR_UNKNOWN => Err(NixError::Unknown { + location, + reason: "Unknown error occurred".to_string(), + }), + _ => Err(NixError::Undocumented { location, err_code }), + } + } + + pub fn from_nulerror( + result: Result, + location: &'static str, + ) -> Result { + result.or(Err(NixError::NulError { location })) + } + + pub fn new_nonnull(ptr: *mut T, location: &'static str) -> Result, Self> + where + T: Sized, + { + NonNull::new(ptr).ok_or(NixError::NullPtr { location }) + } +} + +impl std::error::Error for NixError {} + +impl Display for NixError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let msg = match self { + NixError::NixError { location } => { + format!("[libnix] Generic error (at location `{location}`)") + } + NixError::Overflow { location } => { + format!("[libnix] Overflow error (at location `{location}`)") + } + NixError::KeyNotFound { location, key } => format!( + "[libnix] Key not found {} (at location `{location}`)", + match key { + Some(key) => format!("`{key}`"), + None => "".to_owned(), + } + ), + + NixError::Unknown { location, reason } => { + format!("Unknown error \"{reason}\" (at location `{location}`)") + } + NixError::Undocumented { location, err_code } => { + format!( + "[libnix] An undocumented nix_err was returned with {err_code} (at location `{location}`)" + ) + } + + NixError::NulError { location } => { + format!("Nul error (at location `{location}`)") + } + NixError::NullPtr { location } => { + format!("[libnix] Null pointer (at location `{location}`)") + } + + NixError::InvalidArg { location, reason } => { + format!("Invalid argument \"{reason}\" (at location `{location}`)") + } + NixError::InvalidType { + location, + expected, + got, + } => { + format!("Invalid type, expected \"{expected}\" ${got} (at location `{location}`)") + } + }; + + write!(f, "{msg}") + } +} diff --git a/nixide/src/expr/evalstate.rs b/nixide/src/expr/evalstate.rs new file mode 100644 index 0000000..b0f0220 --- /dev/null +++ b/nixide/src/expr/evalstate.rs @@ -0,0 +1,117 @@ +use std::ffi::CString; +use std::ptr::NonNull; +use std::sync::Arc; + +use super::Value; +use crate::sys; +use crate::{Context, NixError, Store}; + +/// Nix evaluation state for evaluating expressions. +/// +/// This provides the main interface for evaluating Nix expressions +/// and creating values. +pub struct EvalState { + inner: NonNull, + #[allow(dead_code)] + store: Arc, + pub(super) context: Arc, +} + +impl EvalState { + /// Construct a new EvalState directly from its attributes + pub(super) fn new( + inner: NonNull, + store: Arc, + context: Arc, + ) -> Self { + Self { + inner, + store, + context, + } + } + + /// Evaluate a Nix expression from a string. + /// + /// # Arguments + /// + /// * `expr` - The Nix expression to evaluate + /// * `path` - The path to use for error reporting (e.g., "") + /// + /// # Errors + /// + /// Returns an error if evaluation fails. + pub fn eval_from_string(&self, expr: &str, path: &str) -> Result, NixError> { + let expr_c = + NixError::from_nulerror(CString::new(expr), "nixide::EvalState::eval_from_string")?; + let path_c = + NixError::from_nulerror(CString::new(path), "nixide::EvalState::eval_from_string")?; + + // Allocate value for result + // SAFETY: context and state are valid + let value_ptr = unsafe { sys::nix_alloc_value(self.context.as_ptr(), self.inner.as_ptr()) }; + if value_ptr.is_null() { + return Err(NixError::NullPtr { + location: "nix_alloc_value", + }); + } + + // Evaluate expression + // SAFETY: all pointers are valid + NixError::from( + unsafe { + sys::nix_expr_eval_from_string( + self.context.as_ptr(), + self.inner.as_ptr(), + expr_c.as_ptr(), + path_c.as_ptr(), + value_ptr, + ) + }, + "nix_expr_eval_from_string", + )?; + + let inner = NonNull::new(value_ptr).ok_or(NixError::NullPtr { + location: "nix_expr_eval_from_string", + })?; + + Ok(Value { inner, state: self }) + } + + /// Allocate a new value. + /// + /// # Errors + /// + /// Returns an error if value allocation fails. + pub fn alloc_value(&self) -> Result, NixError> { + // SAFETY: context and state are valid + let value_ptr = unsafe { sys::nix_alloc_value(self.context.as_ptr(), self.inner.as_ptr()) }; + let inner = NonNull::new(value_ptr).ok_or(NixError::NullPtr { + location: "nix_alloc_value", + })?; + + Ok(Value { inner, state: self }) + } + + /// Get the raw state pointer. + /// + /// # Safety + /// + /// The caller must ensure the pointer is used safely. + pub(super) unsafe fn as_ptr(&self) -> *mut sys::EvalState { + self.inner.as_ptr() + } +} + +impl Drop for EvalState { + fn drop(&mut self) { + // SAFETY: We own the state and it's valid until drop + unsafe { + sys::nix_state_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: EvalState can be shared between threads +unsafe impl Send for EvalState {} +unsafe impl Sync for EvalState {} diff --git a/nixide/src/expr/evalstatebuilder.rs b/nixide/src/expr/evalstatebuilder.rs new file mode 100644 index 0000000..6ca906c --- /dev/null +++ b/nixide/src/expr/evalstatebuilder.rs @@ -0,0 +1,82 @@ +use std::ptr::NonNull; +use std::sync::Arc; + +use super::EvalState; +use crate::sys; +use crate::{Context, NixError, Store}; + +/// Builder for Nix evaluation state. +/// +/// This allows configuring the evaluation environment before creating +/// the evaluation state. +pub struct EvalStateBuilder { + inner: NonNull, + store: Arc, + context: Arc, +} + +impl EvalStateBuilder { + /// Create a new evaluation state builder. + /// + /// # Arguments + /// + /// * `store` - The Nix store to use for evaluation + /// + /// # Errors + /// + /// Returns an error if the builder cannot be created. + pub fn new(store: &Arc) -> Result { + // SAFETY: store context and store are valid + let builder_ptr = + unsafe { sys::nix_eval_state_builder_new(store._context.as_ptr(), store.as_ptr()) }; + + let inner = NonNull::new(builder_ptr).ok_or(NixError::NullPtr { + location: "nix_eval_state_builder_new", + })?; + + Ok(EvalStateBuilder { + inner, + store: Arc::clone(store), + context: Arc::clone(&store._context), + }) + } + + /// Build the evaluation state. + /// + /// # Errors + /// + /// Returns an error if the evaluation state cannot be built. + pub fn build(self) -> Result { + // Load configuration first + // SAFETY: context and builder are valid + NixError::from( + unsafe { sys::nix_eval_state_builder_load(self.context.as_ptr(), self.inner.as_ptr()) }, + "nix_eval_state_builder_load", + )?; + + // Build the state + // SAFETY: context and builder are valid + let state_ptr = + unsafe { sys::nix_eval_state_build(self.context.as_ptr(), self.inner.as_ptr()) }; + + let inner = NonNull::new(state_ptr).ok_or(NixError::NullPtr { + location: "nix_eval_state_build", + })?; + + // The builder is consumed here - its Drop will clean up + Ok(EvalState::new( + inner, + self.store.clone(), + self.context.clone(), + )) + } +} + +impl Drop for EvalStateBuilder { + fn drop(&mut self) { + // SAFETY: We own the builder and it's valid until drop + unsafe { + sys::nix_eval_state_builder_free(self.inner.as_ptr()); + } + } +} diff --git a/nixide/src/expr/mod.rs b/nixide/src/expr/mod.rs new file mode 100644 index 0000000..ab4ae81 --- /dev/null +++ b/nixide/src/expr/mod.rs @@ -0,0 +1,12 @@ +#[cfg(test)] +mod tests; + +mod evalstate; +mod evalstatebuilder; +mod value; +mod valuetype; + +pub use evalstate::EvalState; +pub use evalstatebuilder::EvalStateBuilder; +pub use value::Value; +pub use valuetype::ValueType; diff --git a/nixide/src/expr/tests.rs b/nixide/src/expr/tests.rs new file mode 100644 index 0000000..688824d --- /dev/null +++ b/nixide/src/expr/tests.rs @@ -0,0 +1,153 @@ +use std::sync::Arc; + +use serial_test::serial; + +use super::{EvalStateBuilder, ValueType}; +use crate::{Context, Store}; + +#[test] +#[serial] +fn test_context_creation() { + let _ctx = Context::new().expect("Failed to create context"); + // Context should be dropped automatically +} + +#[test] +#[serial] +fn test_eval_state_builder() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let _state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + // State should be dropped automatically +} + +#[test] +#[serial] +fn test_simple_evaluation() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + let result = state + .eval_from_string("1 + 2", "") + .expect("Failed to evaluate expression"); + + assert_eq!(result.value_type(), ValueType::Int); + assert_eq!(result.as_int().expect("Failed to get int value"), 3); +} + +#[test] +#[serial] +fn test_value_types() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test integer + let int_val = state + .eval_from_string("42", "") + .expect("Failed to evaluate int"); + assert_eq!(int_val.value_type(), ValueType::Int); + assert_eq!(int_val.as_int().expect("Failed to get int"), 42); + + // Test boolean + let bool_val = state + .eval_from_string("true", "") + .expect("Failed to evaluate bool"); + assert_eq!(bool_val.value_type(), ValueType::Bool); + assert!(bool_val.as_bool().expect("Failed to get bool")); + + // Test string + let str_val = state + .eval_from_string("\"hello\"", "") + .expect("Failed to evaluate string"); + assert_eq!(str_val.value_type(), ValueType::String); + assert_eq!(str_val.as_string().expect("Failed to get string"), "hello"); +} + +#[test] +#[serial] +fn test_value_formatting() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Arc::new(Store::open(&ctx, None).expect("Failed to open store")); + let state = EvalStateBuilder::new(&store) + .expect("Failed to create builder") + .build() + .expect("Failed to build state"); + + // Test integer formatting + let int_val = state + .eval_from_string("42", "") + .expect("Failed to evaluate int"); + assert_eq!(format!("{int_val}"), "42"); + assert_eq!(format!("{int_val:?}"), "Value::Int(42)"); + assert_eq!(int_val.to_nix_string().expect("Failed to format"), "42"); + + // Test boolean formatting + let bool_val = state + .eval_from_string("true", "") + .expect("Failed to evaluate bool"); + assert_eq!(format!("{bool_val}"), "true"); + assert_eq!(format!("{bool_val:?}"), "Value::Bool(true)"); + assert_eq!(bool_val.to_nix_string().expect("Failed to format"), "true"); + + let false_val = state + .eval_from_string("false", "") + .expect("Failed to evaluate bool"); + assert_eq!(format!("{false_val}"), "false"); + assert_eq!( + false_val.to_nix_string().expect("Failed to format"), + "false" + ); + + // Test string formatting + let str_val = state + .eval_from_string("\"hello world\"", "") + .expect("Failed to evaluate string"); + assert_eq!(format!("{str_val}"), "hello world"); + assert_eq!(format!("{str_val:?}"), "Value::String(\"hello world\")"); + assert_eq!( + str_val.to_nix_string().expect("Failed to format"), + "\"hello world\"" + ); + + // Test string with quotes + let quoted_str = state + .eval_from_string("\"say \\\"hello\\\"\"", "") + .expect("Failed to evaluate quoted string"); + assert_eq!(format!("{quoted_str}"), "say \"hello\""); + assert_eq!( + quoted_str.to_nix_string().expect("Failed to format"), + "\"say \\\"hello\\\"\"" + ); + + // Test null formatting + let null_val = state + .eval_from_string("null", "") + .expect("Failed to evaluate null"); + assert_eq!(format!("{null_val}"), "null"); + assert_eq!(format!("{null_val:?}"), "Value::Null"); + assert_eq!(null_val.to_nix_string().expect("Failed to format"), "null"); + + // Test collection formatting + let attrs_val = state + .eval_from_string("{ a = 1; }", "") + .expect("Failed to evaluate attrs"); + assert_eq!(format!("{attrs_val}"), "{ }"); + assert_eq!(format!("{attrs_val:?}"), "Value::Attrs({ })"); + + let list_val = state + .eval_from_string("[ 1 2 3 ]", "") + .expect("Failed to evaluate list"); + assert_eq!(format!("{list_val}"), "[ ]"); + assert_eq!(format!("{list_val:?}"), "Value::List([ ])"); +} diff --git a/nixide/src/expr/value.rs b/nixide/src/expr/value.rs new file mode 100644 index 0000000..fce1b1b --- /dev/null +++ b/nixide/src/expr/value.rs @@ -0,0 +1,322 @@ +use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; +use std::ptr::NonNull; + +use super::{EvalState, ValueType}; +use crate::sys; +use crate::NixError; + +/// A Nix value +/// +/// This represents any value in the Nix language, including primitives, +/// collections, and functions. +pub struct Value<'a> { + pub(crate) inner: NonNull, + pub(crate) state: &'a EvalState, +} + +impl Value<'_> { + /// Force evaluation of this value. + /// + /// If the value is a thunk, this will evaluate it to its final form. + /// + /// # Errors + /// + /// Returns an error if evaluation fails. + pub fn force(&mut self) -> Result<(), NixError> { + NixError::from( + // SAFETY: context, state, and value are valid + unsafe { + sys::nix_value_force( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + ) + }, + "nix_value_force", + ) + } + + /// Force deep evaluation of this value. + /// + /// This forces evaluation of the value and all its nested components. + /// + /// # Errors + /// + /// Returns an error if evaluation fails. + pub fn force_deep(&mut self) -> Result<(), NixError> { + NixError::from( + // SAFETY: context, state, and value are valid + unsafe { + sys::nix_value_force_deep( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + ) + }, + "nix_value_force_deep", + ) + } + + /// Get the type of this value. + #[must_use] + pub fn value_type(&self) -> ValueType { + // SAFETY: context and value are valid + let c_type = unsafe { sys::nix_get_type(self.state.context.as_ptr(), self.inner.as_ptr()) }; + ValueType::from_c(c_type) + } + + /// Convert this value to an integer. + /// + /// # Errors + /// + /// Returns an error if the value is not an integer. + pub fn as_int(&self) -> Result { + if self.value_type() != ValueType::Int { + return Err(NixError::InvalidType { + location: "nixide::Value::as_int", + expected: "int", + got: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let result = unsafe { sys::nix_get_int(self.state.context.as_ptr(), self.inner.as_ptr()) }; + + Ok(result) + } + + /// Convert this value to a float. + /// + /// # Errors + /// + /// Returns an error if the value is not a float. + pub fn as_float(&self) -> Result { + if self.value_type() != ValueType::Float { + return Err(NixError::InvalidType { + location: "nixide::Value::as_float", + expected: "float", + got: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let result = + unsafe { sys::nix_get_float(self.state.context.as_ptr(), self.inner.as_ptr()) }; + + Ok(result) + } + + /// Convert this value to a boolean. + /// + /// # Errors + /// + /// Returns an error if the value is not a boolean. + pub fn as_bool(&self) -> Result { + if self.value_type() != ValueType::Bool { + return Err(NixError::InvalidType { + location: "nixide::Value::as_bool", + expected: "bool", + got: self.value_type().to_string(), + }); + } + + // SAFETY: context and value are valid, type is checked + let result = unsafe { sys::nix_get_bool(self.state.context.as_ptr(), self.inner.as_ptr()) }; + + Ok(result) + } + + /// Convert this value to a string. + /// + /// # Errors + /// + /// Returns an error if the value is not a string. + pub fn as_string(&self) -> Result { + if self.value_type() != ValueType::String { + return Err(NixError::InvalidType { + location: "nixide::Value::as_string", + expected: "string", + got: self.value_type().to_string(), + }); + } + + // For string values, we need to use realised string API + // SAFETY: context and value are valid, type is checked + let realised_str = unsafe { + sys::nix_string_realise( + self.state.context.as_ptr(), + self.state.as_ptr(), + self.inner.as_ptr(), + false, // don't copy more + ) + }; + + if realised_str.is_null() { + return Err(NixError::NullPtr { + location: "nix_string_realise", + }); + } + + // SAFETY: realised_str is non-null and points to valid RealizedString + let buffer_start = unsafe { sys::nix_realised_string_get_buffer_start(realised_str) }; + let buffer_size = unsafe { sys::nix_realised_string_get_buffer_size(realised_str) }; + if buffer_start.is_null() { + // Clean up realised string + unsafe { + sys::nix_realised_string_free(realised_str); + } + return Err(NixError::NullPtr { + location: "nix_realised_string_free", + }); + } + + // SAFETY: buffer_start is non-null and buffer_size gives us the length + let bytes = unsafe { std::slice::from_raw_parts(buffer_start.cast::(), buffer_size) }; + let string = std::str::from_utf8(bytes) + .map_err(|_| NixError::Unknown { + location: "nixide::Value::as_string", + reason: "Invalid UTF-8 in string".to_string(), + })? + .to_owned(); + + // Clean up realised string + unsafe { + sys::nix_realised_string_free(realised_str); + } + + Ok(string) + } + + /// Get the raw value pointer. + /// + /// # Safety + /// + /// The caller must ensure the pointer is used safely. + #[allow(dead_code)] + unsafe fn as_ptr(&self) -> *mut sys::nix_value { + self.inner.as_ptr() + } + + /// Format this value as Nix syntax. + /// + /// This provides a string representation that matches Nix's own syntax, + /// making it useful for debugging and displaying values to users. + /// + /// # Errors + /// + /// Returns an error if the value cannot be converted to a string + /// representation. + pub fn to_nix_string(&self) -> Result { + match self.value_type() { + ValueType::Int => Ok(self.as_int()?.to_string()), + ValueType::Float => Ok(self.as_float()?.to_string()), + ValueType::Bool => Ok(if self.as_bool()? { + "true".to_string() + } else { + "false".to_string() + }), + ValueType::String => Ok(format!("\"{}\"", self.as_string()?.replace('"', "\\\""))), + ValueType::Null => Ok("null".to_string()), + ValueType::Attrs => Ok("{ }".to_string()), + ValueType::List => Ok("[ ]".to_string()), + ValueType::Function => Ok("".to_string()), + ValueType::Path => Ok("".to_string()), + ValueType::Thunk => Ok("".to_string()), + ValueType::External => Ok("".to_string()), + } + } +} + +impl Drop for Value<'_> { + fn drop(&mut self) { + // SAFETY: We own the value and it's valid until drop + unsafe { + sys::nix_value_decref(self.state.context.as_ptr(), self.inner.as_ptr()); + } + } +} + +impl Display for Value<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self.value_type() { + ValueType::Int => { + if let Ok(val) = self.as_int() { + write!(f, "{val}") + } else { + write!(f, "") + } + } + ValueType::Float => { + if let Ok(val) = self.as_float() { + write!(f, "{val}") + } else { + write!(f, "") + } + } + ValueType::Bool => { + if let Ok(val) = self.as_bool() { + write!(f, "{val}") + } else { + write!(f, "") + } + } + ValueType::String => { + if let Ok(val) = self.as_string() { + write!(f, "{val}") + } else { + write!(f, "") + } + } + ValueType::Null => write!(f, "null"), + ValueType::Attrs => write!(f, "{{ }}"), + ValueType::List => write!(f, "[ ]"), + ValueType::Function => write!(f, ""), + ValueType::Path => write!(f, ""), + ValueType::Thunk => write!(f, ""), + ValueType::External => write!(f, ""), + } + } +} + +impl Debug for Value<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let value_type = self.value_type(); + match value_type { + ValueType::Int => { + if let Ok(val) = self.as_int() { + write!(f, "Value::Int({val})") + } else { + write!(f, "Value::Int()") + } + } + ValueType::Float => { + if let Ok(val) = self.as_float() { + write!(f, "Value::Float({val})") + } else { + write!(f, "Value::Float()") + } + } + ValueType::Bool => { + if let Ok(val) = self.as_bool() { + write!(f, "Value::Bool({val})") + } else { + write!(f, "Value::Bool()") + } + } + ValueType::String => { + if let Ok(val) = self.as_string() { + write!(f, "Value::String({val:?})") + } else { + write!(f, "Value::String()") + } + } + ValueType::Null => write!(f, "Value::Null"), + ValueType::Attrs => write!(f, "Value::Attrs({{ }})"), + ValueType::List => write!(f, "Value::List([ ])"), + ValueType::Function => write!(f, "Value::Function()"), + ValueType::Path => write!(f, "Value::Path()"), + ValueType::Thunk => write!(f, "Value::Thunk()"), + ValueType::External => write!(f, "Value::External()"), + } + } +} diff --git a/nixide/src/expr/valuetype.rs b/nixide/src/expr/valuetype.rs new file mode 100644 index 0000000..408bd65 --- /dev/null +++ b/nixide/src/expr/valuetype.rs @@ -0,0 +1,68 @@ +use std::fmt::{Display, Formatter, Result as FmtResult}; + +use crate::sys; + +/// Nix value types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValueType { + /// Thunk (unevaluated expression). + Thunk, + /// Integer value. + Int, + /// Float value. + Float, + /// Boolean value. + Bool, + /// String value. + String, + /// Path value. + Path, + /// Null value. + Null, + /// Attribute set. + Attrs, + /// List. + List, + /// Function. + Function, + /// External value. + External, +} + +impl ValueType { + pub(super) fn from_c(value_type: sys::ValueType) -> Self { + match value_type { + sys::ValueType_NIX_TYPE_THUNK => ValueType::Thunk, + sys::ValueType_NIX_TYPE_INT => ValueType::Int, + sys::ValueType_NIX_TYPE_FLOAT => ValueType::Float, + sys::ValueType_NIX_TYPE_BOOL => ValueType::Bool, + sys::ValueType_NIX_TYPE_STRING => ValueType::String, + sys::ValueType_NIX_TYPE_PATH => ValueType::Path, + sys::ValueType_NIX_TYPE_NULL => ValueType::Null, + sys::ValueType_NIX_TYPE_ATTRS => ValueType::Attrs, + sys::ValueType_NIX_TYPE_LIST => ValueType::List, + sys::ValueType_NIX_TYPE_FUNCTION => ValueType::Function, + sys::ValueType_NIX_TYPE_EXTERNAL => ValueType::External, + _ => ValueType::Thunk, // fallback (TODO: is this ok?) + } + } +} + +impl Display for ValueType { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let name = match self { + ValueType::Thunk => "thunk", + ValueType::Int => "int", + ValueType::Float => "float", + ValueType::Bool => "bool", + ValueType::String => "string", + ValueType::Path => "path", + ValueType::Null => "null", + ValueType::Attrs => "attrs", + ValueType::List => "list", + ValueType::Function => "function", + ValueType::External => "external", + }; + write!(f, "{name}") + } +} diff --git a/nixide/src/lib.rs b/nixide/src/lib.rs index b93cf3f..8dba35d 100644 --- a/nixide/src/lib.rs +++ b/nixide/src/lib.rs @@ -1,14 +1,14 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +// #![warn(missing_docs)] -#[cfg(test)] -mod tests { - use super::*; +mod context; +mod error; +mod expr; +mod store; +mod util; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use context::Context; +pub use error::NixError; +pub use expr::{EvalState, EvalStateBuilder, Value, ValueType}; +pub use store::{Store, StorePath}; + +pub use nixide_sys as sys; diff --git a/nixide/src/store/mod.rs b/nixide/src/store/mod.rs new file mode 100644 index 0000000..668f6b9 --- /dev/null +++ b/nixide/src/store/mod.rs @@ -0,0 +1,275 @@ +// XXX: TODO: should I add support for `nix_libstore_init_no_load_config` +// XXX: TODO: add support for nix_realised_string_* family of functions +// nix_realised_string_get_store_path +// nix_realised_string_get_store_path_count +// # nix_store_real_path +// # nix_store_is_valid_path +// # nix_store_get_version +// # nix_store_get_uri +// # nix_store_get_storedir +// # nix_store_copy_closure +// nix_libstore_init_no_load_config + +#[cfg(test)] +mod tests; + +mod path; +pub use path::*; + +use std::ffi::{CStr, CString, NulError}; +use std::os::raw::{c_char, c_void}; +use std::path::PathBuf; +use std::ptr::NonNull; +use std::result::Result; +use std::sync::Arc; + +use super::{Context, NixError}; +use crate::util::bindings::{wrap_libnix_pathbuf_callback, wrap_libnix_string_callback}; +use nixide_sys as sys; + +/// Nix store for managing packages and derivations. +/// +/// The store provides access to Nix packages, derivations, and store paths. +pub struct Store { + pub(crate) inner: NonNull, + pub(crate) _context: Arc, +} + +impl Store { + /// Open a Nix store. + /// + /// # Arguments + /// + /// * `context` - The Nix context + /// * `uri` - Optional store URI (None for default store) + /// + /// # Errors + /// + /// Returns an error if the store cannot be opened. + pub fn open(context: &Arc, uri: Option<&str>) -> Result { + let uri_cstring: CString; + let uri_ptr = if let Some(uri) = uri { + uri_cstring = NixError::from_nulerror(CString::new(uri), "nixide::Store::open")?; + uri_cstring.as_ptr() + } else { + std::ptr::null() + }; + + // SAFETY: context is valid, uri_ptr is either null or valid CString + let store_ptr = + unsafe { sys::nix_store_open(context.as_ptr(), uri_ptr, std::ptr::null_mut()) }; + + let inner = NonNull::new(store_ptr).ok_or(NixError::NullPtr { + location: "nix_store_open", + })?; + + Ok(Store { + inner, + _context: Arc::clone(context), + }) + } + + /// Get the raw store pointer. + /// + /// # Safety + /// + /// The caller must ensure the pointer is used safely. + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::Store { + self.inner.as_ptr() + } + + /// Realize a store path. + /// + /// This builds/downloads the store path and all its dependencies, + /// making them available in the local store. + /// + /// # Arguments + /// + /// * `path` - The store path to realize + /// + /// # Returns + /// + /// A vector of (output_name, store_path) tuples for each realized output. + /// For example, a derivation might produce outputs like ("out", path1), ("dev", path2). + /// + /// # Errors + /// + /// Returns an error if the path cannot be realized. + pub fn realise( + &self, + path: &StorePath, + callback: fn(&str, &StorePath), + ) -> Result, NixError> { + // Type alias for our userdata: (outputs vector, context) + type Userdata = (Vec<(String, StorePath)>, Arc, fn(&str, &StorePath)); + + // Callback function that will be called for each realized output + unsafe extern "C" fn realise_callback( + userdata: *mut c_void, + out_name_ptr: *const c_char, + out_path_ptr: *const sys::StorePath, + ) { + // SAFETY: userdata is a valid pointer to our (Vec, Arc) tuple + let (outputs, context, callback) = unsafe { &mut *(userdata as *mut Userdata) }; + + // SAFETY: outname is a valid C string from Nix + let output_name = if !out_name_ptr.is_null() { + unsafe { CStr::from_ptr(out_name_ptr).to_string_lossy().into_owned() } + } else { + String::from("out") // Default output name + }; + + // SAFETY: out is a valid StorePath pointer from Nix, we need to clone it + // because Nix owns the original and may free it after the callback + if !out_path_ptr.is_null() { + let cloned_path_ptr = + unsafe { sys::nix_store_path_clone(out_path_ptr as *mut sys::StorePath) }; + if let Some(inner) = NonNull::new(cloned_path_ptr) { + let store_path = StorePath { + inner, + _context: Arc::clone(context), + }; + + callback(output_name.as_ref(), &store_path); + + outputs.push((output_name, store_path)); + } + } + } + + // Create userdata with empty outputs vector and context + let mut userdata: Userdata = (Vec::new(), Arc::clone(&self._context), callback); + let userdata_ptr = &mut userdata as *mut Userdata as *mut std::os::raw::c_void; + + // SAFETY: All pointers are valid, callback is compatible with the FFI signature + // - self._context is valid for the duration of this call + // - self.inner is valid (checked in Store::open) + // - path.inner is valid (checked in StorePath::parse) + // - userdata_ptr points to valid stack memory + // - realize_callback matches the expected C function signature + let err = unsafe { + sys::nix_store_realise( + self._context.as_ptr(), + self.inner.as_ptr(), + path.as_ptr(), + userdata_ptr, + Some(realise_callback), + ) + }; + + NixError::from(err, "nix_store_realise")?; + + // Return the collected outputs + Ok(userdata.0) + } + + /// Parse a store path string into a StorePath. + /// + /// This is a convenience method that wraps `StorePath::parse()`. + /// + /// # Arguments + /// + /// * `path` - The store path string (e.g., "/nix/store/...") + /// + /// # Errors + /// + /// Returns an error if the path cannot be parsed. + /// + /// # Example + /// + /// ```no_run + /// # use std::sync::Arc; + /// # use nixide::{Context, Store}; + /// # fn main() -> Result<(), Box> { + /// let ctx = Arc::new(Context::new()?); + /// let store = Store::open(&ctx, None)?; + /// let path = store.store_path("/nix/store/...")?; + /// # Ok(()) + /// # } + /// ``` + pub fn store_path(&self, path: &str) -> Result { + StorePath::parse(&self._context, self, path) + } + + /// Get the version of a Nix store + /// + /// If the store doesn't have a version (like the dummy store), returns None + pub fn version(&self) -> Result { + wrap_libnix_string_callback("nix_store_get_version", |callback, user_data| unsafe { + sys::nix_store_get_version( + self._context.as_ptr(), + self.inner.as_ptr(), + Some(callback), + user_data, + ) + }) + } + + /// Get the URI of a Nix store + pub fn uri(&self) -> Result { + wrap_libnix_string_callback("nix_store_get_uri", |callback, user_data| unsafe { + sys::nix_store_get_uri( + self._context.as_ptr(), + self.inner.as_ptr(), + Some(callback), + user_data, + ) + }) + } + + pub fn store_dir(&self) -> Result { + wrap_libnix_pathbuf_callback("nix_store_get_storedir", |callback, user_data| unsafe { + sys::nix_store_get_storedir( + self._context.as_ptr(), + self.inner.as_ptr(), + Some(callback), + user_data, + ) + }) + } + + pub fn copy_closure_to( + &self, + dst_store: &Store, + store_path: &StorePath, + ) -> Result<(), NixError> { + let err = unsafe { + sys::nix_store_copy_closure( + self._context.as_ptr(), + self.inner.as_ptr(), + dst_store.inner.as_ptr(), + store_path.inner.as_ptr(), + ) + }; + NixError::from(err, "nix_store_copy_closure") + } + + pub fn copy_closure_from( + &self, + src_store: &Store, + store_path: &StorePath, + ) -> Result<(), NixError> { + let err = unsafe { + sys::nix_store_copy_closure( + self._context.as_ptr(), + src_store.inner.as_ptr(), + self.inner.as_ptr(), + store_path.inner.as_ptr(), + ) + }; + NixError::from(err, "nix_store_copy_closure") + } +} + +impl Drop for Store { + fn drop(&mut self) { + // SAFETY: We own the store and it's valid until drop + unsafe { + sys::nix_store_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: Store can be shared between threads +unsafe impl Send for Store {} +unsafe impl Sync for Store {} diff --git a/nixide/src/store/path.rs b/nixide/src/store/path.rs new file mode 100644 index 0000000..8b8b6e4 --- /dev/null +++ b/nixide/src/store/path.rs @@ -0,0 +1,156 @@ +use std::ffi::CString; +use std::path::PathBuf; +use std::ptr::NonNull; +use std::sync::Arc; + +use super::Store; +use crate::util::bindings::{wrap_libnix_pathbuf_callback, wrap_libnix_string_callback}; +use crate::{Context, NixError}; +use nixide_sys::{self as sys, nix_err_NIX_OK}; + +/// A path in the Nix store. +/// +/// Represents a store path that can be realized, queried, or manipulated. +pub struct StorePath { + pub(crate) inner: NonNull, + pub(crate) _context: Arc, +} + +impl StorePath { + /// Parse a store path string into a StorePath. + /// + /// # Arguments + /// + /// * `context` - The Nix context + /// * `store` - The store containing the path + /// * `path` - The store path string (e.g., "/nix/store/...") + /// + /// # Errors + /// + /// Returns an error if the path cannot be parsed. + pub fn parse(context: &Arc, store: &Store, path: &str) -> Result { + let path_cstring = CString::new(path).or(Err(NixError::InvalidArg { + location: "nixide::StorePath::parse", + reason: "`path` contains NUL char", + }))?; + + // SAFETY: context, store, and path_cstring are valid + let path_ptr = unsafe { + sys::nix_store_parse_path(context.as_ptr(), store.as_ptr(), path_cstring.as_ptr()) + }; + + let inner = NonNull::new(path_ptr).ok_or(NixError::NullPtr { + location: "nix_store_parse_path", + })?; + + Ok(Self { + inner, + _context: Arc::clone(context), + }) + } + + /// Get the name component of the store path. + /// + /// This returns the name part of the store path (everything after the hash). + /// For example, for "/nix/store/abc123...-hello-1.0", this returns "hello-1.0". + /// + /// # Errors + /// + /// Returns an error if the name cannot be retrieved. + pub fn name(&self) -> Result { + wrap_libnix_string_callback("nix_store_path_name", |callback, user_data| unsafe { + sys::nix_store_path_name(self.inner.as_ptr(), Some(callback), user_data); + + // NOTE: nix_store_path_name doesn't return nix_err, so we force it to return successfully + nix_err_NIX_OK + }) + } + + /// Get the physical location of a store path + /// + /// A store may reside at a different location than its `storeDir` suggests. + /// This situation is called a relocated store. + /// + /// Relocated stores are used during NixOS installation, as well as in restricted + /// computing environments that don't offer a writable `/nix/store`. + /// + /// Not all types of stores support this operation. + /// + /// # Arguments + /// * `context` [in] - Optional, stores error information + /// * `store` [in] - nix store reference + /// * `path` [in] - the path to get the real path from + /// * `callback` [in] - called with the real path + /// * `user_data` [in] - arbitrary data, passed to the callback when it's called. + /// + /// # Arguments + /// + /// * `store` - The store containing the path + /// + pub fn real_path(&self, store: &Store) -> Result { + wrap_libnix_pathbuf_callback("nix_store_real_path", |callback, user_data| unsafe { + sys::nix_store_real_path( + self._context.as_ptr(), + store.inner.as_ptr(), + self.inner.as_ptr(), + Some(callback), + user_data, + ) + }) + } + + /// Check if a [StorePath] is valid (i.e. that its corresponding store object + /// and its closure of references exists in the store). + /// + /// # Arguments + /// + /// * `store` - The store containing the path + /// + pub fn is_valid(&self, store: &Store) -> bool { + unsafe { + sys::nix_store_is_valid_path( + self._context.as_ptr(), + store.inner.as_ptr(), + self.inner.as_ptr(), + ) + } + } + + /// Get the raw store path pointer. + /// + /// # Safety + /// + /// The caller must ensure the pointer is used safely. + pub(crate) unsafe fn as_ptr(&self) -> *mut sys::StorePath { + self.inner.as_ptr() + } +} + +impl Clone for StorePath { + fn clone(&self) -> Self { + // SAFETY: self.inner is valid, nix_store_path_clone creates a new copy + let cloned_ptr = unsafe { sys::nix_store_path_clone(self.inner.as_ptr()) }; + + // This should never fail as cloning a valid path should always succeed + let inner = + NonNull::new(cloned_ptr).expect("nix_store_path_clone returned null for valid path"); + + StorePath { + inner, + _context: Arc::clone(&self._context), + } + } +} + +impl Drop for StorePath { + fn drop(&mut self) { + // SAFETY: We own the store path and it's valid until drop + unsafe { + sys::nix_store_path_free(self.inner.as_ptr()); + } + } +} + +// SAFETY: StorePath can be shared between threads +unsafe impl Send for StorePath {} +unsafe impl Sync for StorePath {} diff --git a/nixide/src/store/tests.rs b/nixide/src/store/tests.rs new file mode 100644 index 0000000..e00c3c9 --- /dev/null +++ b/nixide/src/store/tests.rs @@ -0,0 +1,66 @@ +use serial_test::serial; + +use super::*; + +#[test] +#[serial] +fn test_store_opening() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let _store = Store::open(&ctx, None).expect("Failed to open store"); +} + +#[test] +#[serial] +fn test_store_path_parse() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + + // Try parsing a well-formed store path + // Note: This may fail if the path doesn't exist in the store + let result = StorePath::parse( + &ctx, + &store, + "/nix/store/00000000000000000000000000000000-test", + ); + + // We don't assert success here because the path might not exist + // This test mainly checks that the API works correctly + match result { + Ok(_path) => { + // Successfully parsed the path + } + Err(_) => { + // Path doesn't exist or is invalid, which is expected + } + } +} + +#[test] +#[serial] +fn test_store_path_clone() { + let ctx = Arc::new(Context::new().expect("Failed to create context")); + let store = Store::open(&ctx, None).expect("Failed to open store"); + + // Try to get a valid store path by parsing + // Note: This test is somewhat limited without a guaranteed valid path + if let Ok(path) = StorePath::parse( + &ctx, + &store, + "/nix/store/00000000000000000000000000000000-test", + ) { + let cloned = path.clone(); + + // Assert that the cloned path has the same name as the original + let original_name = path.name().expect("Failed to get original path name"); + let cloned_name = cloned.name().expect("Failed to get cloned path name"); + + assert_eq!( + original_name, cloned_name, + "Cloned path should have the same name as original" + ); + } +} + +// Note: test_realize is not included because it requires a valid store path +// to realize, which we can't guarantee in a unit test. Integration tests +// would be more appropriate for testing realize() with actual derivations. diff --git a/nixide/src/util/bindings.rs b/nixide/src/util/bindings.rs new file mode 100644 index 0000000..ea9b45f --- /dev/null +++ b/nixide/src/util/bindings.rs @@ -0,0 +1,34 @@ +use std::os::raw::{c_char, c_uint, c_void}; +use std::path::PathBuf; + +use crate::NixError; + +pub fn wrap_libnix_string_callback(name: &'static str, callback: F) -> Result +where + F: FnOnce(unsafe extern "C" fn(*const c_char, c_uint, *mut c_void), *mut c_void) -> i32, +{ + // Callback to receive the string + unsafe extern "C" fn wrapper_callback(start: *const c_char, n: c_uint, user_data: *mut c_void) { + let result = unsafe { &mut *(user_data as *mut Option) }; + + if !start.is_null() && n > 0 { + let bytes = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + if let Ok(s) = std::str::from_utf8(bytes) { + *result = Some(s.to_string()); + } + } + } + + let mut result: Option = None; + let user_data = &mut result as *mut _ as *mut c_void; + + NixError::from(callback(wrapper_callback, user_data), name)?; + result.ok_or(NixError::NullPtr { location: name }) +} + +pub fn wrap_libnix_pathbuf_callback(name: &'static str, callback: F) -> Result +where + F: FnOnce(unsafe extern "C" fn(*const c_char, c_uint, *mut c_void), *mut c_void) -> i32, +{ + wrap_libnix_string_callback(name, callback).map(PathBuf::from) +} diff --git a/nixide/src/util/mod.rs b/nixide/src/util/mod.rs new file mode 100644 index 0000000..90c70dc --- /dev/null +++ b/nixide/src/util/mod.rs @@ -0,0 +1 @@ +pub mod bindings;