add expr and store support
This commit is contained in:
parent
ae6251c3a1
commit
14859de6a6
16 changed files with 1557 additions and 13 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -191,6 +191,7 @@ name = "nixide"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nixide-sys",
|
||||
"serial_test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ license = "GPL-3.0"
|
|||
repository = "https://codeberg.org/luminary/nixide"
|
||||
authors = [
|
||||
"_cry64 <them@dobutterfliescry.net>",
|
||||
"foxxyora <foxxyora@noreply.codeberg.org>"
|
||||
"foxxyora <foxxyora@noreply.codeberg.org>",
|
||||
]
|
||||
edition = "2024"
|
||||
|
||||
|
|
@ -25,3 +25,6 @@ gc = []
|
|||
|
||||
[dependencies]
|
||||
nixide-sys = { path = "../nixide-sys", version = "0.1.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "3.4.0"
|
||||
|
|
|
|||
71
nixide/src/context.rs
Normal file
71
nixide/src/context.rs
Normal file
|
|
@ -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<sys::nix_c_context>,
|
||||
}
|
||||
|
||||
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<Self, NixError> {
|
||||
// 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 {}
|
||||
183
nixide/src/error.rs
Normal file
183
nixide/src/error.rs
Normal file
|
|
@ -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<String>,
|
||||
},
|
||||
|
||||
/// 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<T>(
|
||||
result: Result<T, NulError>,
|
||||
location: &'static str,
|
||||
) -> Result<T, Self> {
|
||||
result.or(Err(NixError::NulError { location }))
|
||||
}
|
||||
|
||||
pub fn new_nonnull<T>(ptr: *mut T, location: &'static str) -> Result<NonNull<T>, 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}")
|
||||
}
|
||||
}
|
||||
117
nixide/src/expr/evalstate.rs
Normal file
117
nixide/src/expr/evalstate.rs
Normal file
|
|
@ -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<sys::EvalState>,
|
||||
#[allow(dead_code)]
|
||||
store: Arc<Store>,
|
||||
pub(super) context: Arc<Context>,
|
||||
}
|
||||
|
||||
impl EvalState {
|
||||
/// Construct a new EvalState directly from its attributes
|
||||
pub(super) fn new(
|
||||
inner: NonNull<sys::EvalState>,
|
||||
store: Arc<Store>,
|
||||
context: Arc<Context>,
|
||||
) -> 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., "<eval>")
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if evaluation fails.
|
||||
pub fn eval_from_string(&self, expr: &str, path: &str) -> Result<Value<'_>, 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<Value<'_>, 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 {}
|
||||
82
nixide/src/expr/evalstatebuilder.rs
Normal file
82
nixide/src/expr/evalstatebuilder.rs
Normal file
|
|
@ -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<sys::nix_eval_state_builder>,
|
||||
store: Arc<Store>,
|
||||
context: Arc<Context>,
|
||||
}
|
||||
|
||||
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<Store>) -> Result<Self, NixError> {
|
||||
// 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<EvalState, NixError> {
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
12
nixide/src/expr/mod.rs
Normal file
12
nixide/src/expr/mod.rs
Normal file
|
|
@ -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;
|
||||
153
nixide/src/expr/tests.rs
Normal file
153
nixide/src/expr/tests.rs
Normal file
|
|
@ -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", "<eval>")
|
||||
.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", "<eval>")
|
||||
.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", "<eval>")
|
||||
.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\"", "<eval>")
|
||||
.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", "<eval>")
|
||||
.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", "<eval>")
|
||||
.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", "<eval>")
|
||||
.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\"", "<eval>")
|
||||
.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\\\"\"", "<eval>")
|
||||
.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", "<eval>")
|
||||
.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; }", "<eval>")
|
||||
.expect("Failed to evaluate attrs");
|
||||
assert_eq!(format!("{attrs_val}"), "{ <attrs> }");
|
||||
assert_eq!(format!("{attrs_val:?}"), "Value::Attrs({ <attrs> })");
|
||||
|
||||
let list_val = state
|
||||
.eval_from_string("[ 1 2 3 ]", "<eval>")
|
||||
.expect("Failed to evaluate list");
|
||||
assert_eq!(format!("{list_val}"), "[ <list> ]");
|
||||
assert_eq!(format!("{list_val:?}"), "Value::List([ <list> ])");
|
||||
}
|
||||
322
nixide/src/expr/value.rs
Normal file
322
nixide/src/expr/value.rs
Normal file
|
|
@ -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<sys::nix_value>,
|
||||
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<i64, NixError> {
|
||||
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<f64, NixError> {
|
||||
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<bool, NixError> {
|
||||
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<String, NixError> {
|
||||
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::<u8>(), 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<String, NixError> {
|
||||
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("{ <attrs> }".to_string()),
|
||||
ValueType::List => Ok("[ <list> ]".to_string()),
|
||||
ValueType::Function => Ok("<function>".to_string()),
|
||||
ValueType::Path => Ok("<path>".to_string()),
|
||||
ValueType::Thunk => Ok("<thunk>".to_string()),
|
||||
ValueType::External => Ok("<external>".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, "<int error>")
|
||||
}
|
||||
}
|
||||
ValueType::Float => {
|
||||
if let Ok(val) = self.as_float() {
|
||||
write!(f, "{val}")
|
||||
} else {
|
||||
write!(f, "<float error>")
|
||||
}
|
||||
}
|
||||
ValueType::Bool => {
|
||||
if let Ok(val) = self.as_bool() {
|
||||
write!(f, "{val}")
|
||||
} else {
|
||||
write!(f, "<bool error>")
|
||||
}
|
||||
}
|
||||
ValueType::String => {
|
||||
if let Ok(val) = self.as_string() {
|
||||
write!(f, "{val}")
|
||||
} else {
|
||||
write!(f, "<string error>")
|
||||
}
|
||||
}
|
||||
ValueType::Null => write!(f, "null"),
|
||||
ValueType::Attrs => write!(f, "{{ <attrs> }}"),
|
||||
ValueType::List => write!(f, "[ <list> ]"),
|
||||
ValueType::Function => write!(f, "<function>"),
|
||||
ValueType::Path => write!(f, "<path>"),
|
||||
ValueType::Thunk => write!(f, "<thunk>"),
|
||||
ValueType::External => write!(f, "<external>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(<error>)")
|
||||
}
|
||||
}
|
||||
ValueType::Float => {
|
||||
if let Ok(val) = self.as_float() {
|
||||
write!(f, "Value::Float({val})")
|
||||
} else {
|
||||
write!(f, "Value::Float(<error>)")
|
||||
}
|
||||
}
|
||||
ValueType::Bool => {
|
||||
if let Ok(val) = self.as_bool() {
|
||||
write!(f, "Value::Bool({val})")
|
||||
} else {
|
||||
write!(f, "Value::Bool(<error>)")
|
||||
}
|
||||
}
|
||||
ValueType::String => {
|
||||
if let Ok(val) = self.as_string() {
|
||||
write!(f, "Value::String({val:?})")
|
||||
} else {
|
||||
write!(f, "Value::String(<error>)")
|
||||
}
|
||||
}
|
||||
ValueType::Null => write!(f, "Value::Null"),
|
||||
ValueType::Attrs => write!(f, "Value::Attrs({{ <attrs> }})"),
|
||||
ValueType::List => write!(f, "Value::List([ <list> ])"),
|
||||
ValueType::Function => write!(f, "Value::Function(<function>)"),
|
||||
ValueType::Path => write!(f, "Value::Path(<path>)"),
|
||||
ValueType::Thunk => write!(f, "Value::Thunk(<thunk>)"),
|
||||
ValueType::External => write!(f, "Value::External(<external>)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
68
nixide/src/expr/valuetype.rs
Normal file
68
nixide/src/expr/valuetype.rs
Normal file
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
275
nixide/src/store/mod.rs
Normal file
275
nixide/src/store/mod.rs
Normal file
|
|
@ -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<sys::Store>,
|
||||
pub(crate) _context: Arc<Context>,
|
||||
}
|
||||
|
||||
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<Context>, uri: Option<&str>) -> Result<Self, NixError> {
|
||||
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<F>(
|
||||
&self,
|
||||
path: &StorePath,
|
||||
callback: fn(&str, &StorePath),
|
||||
) -> Result<Vec<(String, StorePath)>, NixError> {
|
||||
// Type alias for our userdata: (outputs vector, context)
|
||||
type Userdata = (Vec<(String, StorePath)>, Arc<Context>, 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<Context>) 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<dyn std::error::Error>> {
|
||||
/// 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, NixError> {
|
||||
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<String, NixError> {
|
||||
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<String, NixError> {
|
||||
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<PathBuf, NixError> {
|
||||
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 {}
|
||||
156
nixide/src/store/path.rs
Normal file
156
nixide/src/store/path.rs
Normal file
|
|
@ -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<sys::StorePath>,
|
||||
pub(crate) _context: Arc<Context>,
|
||||
}
|
||||
|
||||
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<Context>, store: &Store, path: &str) -> Result<Self, NixError> {
|
||||
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<String, NixError> {
|
||||
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<PathBuf, NixError> {
|
||||
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 {}
|
||||
66
nixide/src/store/tests.rs
Normal file
66
nixide/src/store/tests.rs
Normal file
|
|
@ -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.
|
||||
34
nixide/src/util/bindings.rs
Normal file
34
nixide/src/util/bindings.rs
Normal file
|
|
@ -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<F>(name: &'static str, callback: F) -> Result<String, NixError>
|
||||
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<String>) };
|
||||
|
||||
if !start.is_null() && n > 0 {
|
||||
let bytes = unsafe { std::slice::from_raw_parts(start.cast::<u8>(), n as usize) };
|
||||
if let Ok(s) = std::str::from_utf8(bytes) {
|
||||
*result = Some(s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result: Option<String> = 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<F>(name: &'static str, callback: F) -> Result<PathBuf, NixError>
|
||||
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)
|
||||
}
|
||||
1
nixide/src/util/mod.rs
Normal file
1
nixide/src/util/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod bindings;
|
||||
Loading…
Add table
Add a link
Reference in a new issue