From e9022e675b2753cfa6cff6d4973081477a5e85a8 Mon Sep 17 00:00:00 2001 From: _cry64 Date: Fri, 13 Mar 2026 23:20:49 +1000 Subject: [PATCH] working sys level bindings :yippie: --- .gitignore | 3 - Cargo.lock | 148 ++++ nixide-sys/Cargo.toml | 5 +- nixide-sys/build.rs | 44 +- nixide-sys/include/wrapper.h | 24 +- nixide-sys/lib.rs | 27 +- nixide-sys/tests/eval.rs | 1235 ++++++++++++++++++++++++++++++++++ nixide-sys/tests/flake.rs | 78 +++ nixide-sys/tests/memory.rs | 503 ++++++++++++++ nixide-sys/tests/primop.rs | 639 ++++++++++++++++++ nixide-sys/tests/store.rs | 278 ++++++++ nixide-sys/tests/util.rs | 152 +++++ 12 files changed, 3091 insertions(+), 45 deletions(-) create mode 100644 nixide-sys/tests/eval.rs create mode 100644 nixide-sys/tests/flake.rs create mode 100644 nixide-sys/tests/memory.rs create mode 100644 nixide-sys/tests/primop.rs create mode 100644 nixide-sys/tests/store.rs create mode 100644 nixide-sys/tests/util.rs diff --git a/.gitignore b/.gitignore index b36bbaf..e6857d8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,3 @@ result-* # Rust /target - -# Other -nixops4-bindings-sys diff --git a/Cargo.lock b/Cargo.lock index cac84e4..dfba9b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,41 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "glob" version = "0.3.3" @@ -124,6 +159,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -150,6 +194,7 @@ dependencies = [ "cc", "doxygen-bindgen", "pkg-config", + "serial_test", ] [[package]] @@ -162,6 +207,41 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkg-config" version = "0.3.32" @@ -186,6 +266,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -221,12 +310,71 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "syn" version = "2.0.117" diff --git a/nixide-sys/Cargo.toml b/nixide-sys/Cargo.toml index cc2c8a2..0be064c 100644 --- a/nixide-sys/Cargo.toml +++ b/nixide-sys/Cargo.toml @@ -4,10 +4,9 @@ description = "Unsafe direct FFI bindings to libnix C API" version = "0.1.0" readme = "../README.md" license = "GPL-3.0" -license-file = "../LICENSE" repository = "https://codeberg.org/luminary/nixide" authors = [ - "_cry64 " + "_cry64 ", "foxxyora " ] @@ -35,3 +34,5 @@ doxygen-bindgen = "0.1.3" pkg-config.workspace = true cc.workspace = true +[dev-dependencies] +serial_test = "3.4.0" diff --git a/nixide-sys/build.rs b/nixide-sys/build.rs index 6d20acf..4419125 100644 --- a/nixide-sys/build.rs +++ b/nixide-sys/build.rs @@ -1,6 +1,5 @@ use std::env; use std::path::PathBuf; -use std::process::Command; use bindgen::callbacks::ParseCallbacks; @@ -24,7 +23,7 @@ fn main() { println!("cargo:rerun-if-changed=include/wrapper.h"); // Tell cargo to tell rustc to link the system shared library - println!("cargo:rustc-link-lib=bz2"); + // println!("cargo:rustc-link-lib=bz2"); // Use pkg-config to find nix-store include and link paths // This NEEDS to be included, or otherwise `nix_api_store.h` cannot @@ -37,32 +36,23 @@ fn main() { "nix-flake-c", ]; - // Add all pkg-config include paths and GCC's include path to bindgen - let mut args = Vec::new(); - for nix_lib in libs { - let lib = pkg_config::probe_library(nix_lib) - .expect(&format!("Unable to find .pc file for {}", nix_lib)); - - for include_path in lib.include_paths { - args.push(format!("-I{}", incloude_path.display())); - // builder = builder.clang_arg(format!("-I{}", include_path.display())); - } - for link_file in lib.link_files { - println!("cargo:rustc-link-lib={}", link_file.display()); - } - } + let lib_args: Vec = libs + .iter() + .map(|&name| { + let lib = pkg_config::probe_library(name) + .expect(&format!("Unable to find .pc file for {}", name)); - let lib_args = libs.map(|name| { - let lib = pkg_config::probe_library(name) - .expect(&format!("Unable to find .pc file for {}", nix_lib)); + for p in lib.link_files { + println!("cargo:rustc-link-lib={}", p.display()); + } + + lib.include_paths + .into_iter() + .map(|p| format!("-I{}", p.display())) + }) + .flatten() + .collect(); - for p in lib.link_files { - println!("cargo:rustc-link-lib={}", p.display()); - } - - lib.include_paths.map(|p| format!("-I{}", p.display())) - }).flatten(); - let bindings = bindgen::Builder::default() .clang_args(lib_args) // The input header we would like to generate bindings for @@ -73,7 +63,7 @@ fn main() { .parse_callbacks(Box::new(DoxygenCallbacks)) // Format generated bindings with rustfmt .formatter(bindgen::Formatter::Rustfmt) - .rustfmt_configuration_file(std::fs::canonicalize(".rustfmt.toml").ok()); + .rustfmt_configuration_file(std::fs::canonicalize(".rustfmt.toml").ok()) // Finish the builder and generate the bindings .generate() // Unwrap the Result and panic on failure diff --git a/nixide-sys/include/wrapper.h b/nixide-sys/include/wrapper.h index 41450d5..30dd94e 100644 --- a/nixide-sys/include/wrapper.h +++ b/nixide-sys/include/wrapper.h @@ -1 +1,23 @@ -#include <> +// Pure C API for store operations +#include + +// Pure C API for error handling +#include + +// Pure C API for the Nix evaluator +#include + +// Pure C API for external values +#include + +// Pure C API for value manipulation +#include + +// Pure C API for fetcher operations +#include + +// Pure C API for flake support +#include + +// Pure C API for main/CLI support +#include diff --git a/nixide-sys/lib.rs b/nixide-sys/lib.rs index b93cf3f..4c744cd 100644 --- a/nixide-sys/lib.rs +++ b/nixide-sys/lib.rs @@ -1,14 +1,17 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +//! # nixide-sys +//! +//! Unsafe direct FFI bindings to libnix C API. +//! +//! ## Safety +//! +//! These bindings are generated automatically and map directly to the C API. +//! They are unsafe to use directly. Prefer using the high-level safe API in the +//! parent crate unless you know what you're doing. -#[cfg(test)] -mod tests { - use super::*; +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(rustdoc::bare_urls)] +#![allow(rustdoc::invalid_html_tags)] - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/nixide-sys/tests/eval.rs b/nixide-sys/tests/eval.rs new file mode 100644 index 0000000..742b4d0 --- /dev/null +++ b/nixide-sys/tests/eval.rs @@ -0,0 +1,1235 @@ +#![cfg(test)] + +use std::{ + ffi::{CStr, CString}, + ptr, +}; + +use nixide_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn eval_init_and_state_build() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libutil_init failed: {err}"); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libstore_init failed: {err}"); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libexpr_init failed: {err}"); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn eval_simple_expression() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libutil_init failed: {err}"); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libstore_init failed: {err}"); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK, "nix_libexpr_init failed: {err}"); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Evaluate a simple integer expression + let expr = CString::new("1 + 2").unwrap(); + let path = CString::new("").unwrap(); + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // Force the value (should not be a thunk) + let force_err = nix_value_force(ctx, state, value); + assert_eq!(force_err, nix_err_NIX_OK); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn value_construction_and_inspection() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Int + let int_val = nix_alloc_value(ctx, state); + assert!(!int_val.is_null()); + assert_eq!(nix_init_int(ctx, int_val, 42), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, int_val), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, int_val), 42); + + // Float + let float_val = nix_alloc_value(ctx, state); + assert!(!float_val.is_null()); + assert_eq!( + nix_init_float(ctx, float_val, std::f64::consts::PI), + nix_err_NIX_OK + ); + assert_eq!(nix_get_type(ctx, float_val), ValueType_NIX_TYPE_FLOAT); + assert!((nix_get_float(ctx, float_val) - std::f64::consts::PI).abs() < 1e-10); + + // Bool + let bool_val = nix_alloc_value(ctx, state); + assert!(!bool_val.is_null()); + assert_eq!(nix_init_bool(ctx, bool_val, true), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, bool_val), ValueType_NIX_TYPE_BOOL); + assert!(nix_get_bool(ctx, bool_val)); + + // Null + let null_val = nix_alloc_value(ctx, state); + assert!(!null_val.is_null()); + assert_eq!(nix_init_null(ctx, null_val), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, null_val), ValueType_NIX_TYPE_NULL); + + // String + let string_val = nix_alloc_value(ctx, state); + assert!(!string_val.is_null()); + let s = CString::new("hello world").unwrap(); + assert_eq!(nix_init_string(ctx, string_val, s.as_ptr()), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, string_val), ValueType_NIX_TYPE_STRING); + extern "C" fn string_cb( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + let mut got: Option = None; + assert_eq!( + nix_get_string(ctx, string_val, Some(string_cb), (&raw mut got).cast()), + nix_err_NIX_OK + ); + assert_eq!(got.as_deref(), Some("hello world")); + + // Path string + let path_val = nix_alloc_value(ctx, state); + assert!(!path_val.is_null()); + let p = CString::new("/nix/store/foo").unwrap(); + assert_eq!( + nix_init_path_string(ctx, state, path_val, p.as_ptr()), + nix_err_NIX_OK + ); + assert_eq!(nix_get_type(ctx, path_val), ValueType_NIX_TYPE_PATH); + let path_ptr = nix_get_path_string(ctx, path_val); + assert!(!path_ptr.is_null()); + let path_str = CStr::from_ptr(path_ptr).to_string_lossy(); + assert_eq!(path_str, "/nix/store/foo"); + + // Clean up + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn list_and_attrset_manipulation() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // List: [1, 2, 3] + let list_builder = nix_make_list_builder(ctx, state, 3); + assert!(!list_builder.is_null()); + let v1 = nix_alloc_value(ctx, state); + let v2 = nix_alloc_value(ctx, state); + let v3 = nix_alloc_value(ctx, state); + nix_init_int(ctx, v1, 1); + nix_init_int(ctx, v2, 2); + nix_init_int(ctx, v3, 3); + nix_list_builder_insert(ctx, list_builder, 0, v1); + nix_list_builder_insert(ctx, list_builder, 1, v2); + nix_list_builder_insert(ctx, list_builder, 2, v3); + + let list_val = nix_alloc_value(ctx, state); + assert_eq!(nix_make_list(ctx, list_builder, list_val), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, list_val), ValueType_NIX_TYPE_LIST); + assert_eq!(nix_get_list_size(ctx, list_val), 3); + + // Get elements by index + for i in 0..3 { + let elem = nix_get_list_byidx(ctx, list_val, state, i); + assert!(!elem.is_null()); + assert_eq!(nix_get_type(ctx, elem), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, elem), i64::from(i + 1)); + } + + nix_list_builder_free(list_builder); + + // Attrset: { foo = 42; bar = "baz"; } + let attr_builder = nix_make_bindings_builder(ctx, state, 2); + assert!(!attr_builder.is_null()); + let foo_val = nix_alloc_value(ctx, state); + let bar_val = nix_alloc_value(ctx, state); + nix_init_int(ctx, foo_val, 42); + let baz = CString::new("baz").unwrap(); + nix_init_string(ctx, bar_val, baz.as_ptr()); + let foo = CString::new("foo").unwrap(); + let bar = CString::new("bar").unwrap(); + nix_bindings_builder_insert(ctx, attr_builder, foo.as_ptr(), foo_val); + nix_bindings_builder_insert(ctx, attr_builder, bar.as_ptr(), bar_val); + + let attr_val = nix_alloc_value(ctx, state); + assert_eq!(nix_make_attrs(ctx, attr_val, attr_builder), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, attr_val), ValueType_NIX_TYPE_ATTRS); + assert_eq!(nix_get_attrs_size(ctx, attr_val), 2); + + // Get by name + let foo_got = nix_get_attr_byname(ctx, attr_val, state, foo.as_ptr()); + assert!(!foo_got.is_null()); + assert_eq!(nix_get_type(ctx, foo_got), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, foo_got), 42); + + let bar_got = nix_get_attr_byname(ctx, attr_val, state, bar.as_ptr()); + assert!(!bar_got.is_null()); + assert_eq!(nix_get_type(ctx, bar_got), ValueType_NIX_TYPE_STRING); + + // Has attr + assert!(nix_has_attr_byname(ctx, attr_val, state, foo.as_ptr())); + assert!(nix_has_attr_byname(ctx, attr_val, state, bar.as_ptr())); + + nix_bindings_builder_free(attr_builder); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn function_application_and_force() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Evaluate a function and apply it: (x: x + 1) 41 + let expr = CString::new("(x: x + 1)").unwrap(); + let path = CString::new("").unwrap(); + let fn_val = nix_alloc_value(ctx, state); + assert!(!fn_val.is_null()); + assert_eq!( + nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), fn_val), + nix_err_NIX_OK + ); + + // Argument: 41 + let arg_val = nix_alloc_value(ctx, state); + nix_init_int(ctx, arg_val, 41); + + // Result value + let result_val = nix_alloc_value(ctx, state); + assert!(!result_val.is_null()); + assert_eq!( + nix_value_call(ctx, state, fn_val, arg_val, result_val), + nix_err_NIX_OK + ); + + // Force result + assert_eq!(nix_value_force(ctx, state, result_val), nix_err_NIX_OK); + assert_eq!(nix_get_type(ctx, result_val), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, result_val), 42); + + // Deep force (should be a no-op for int) + assert_eq!(nix_value_force_deep(ctx, state, result_val), nix_err_NIX_OK); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn error_handling_invalid_expression() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Invalid expression + let expr = CString::new("this is not valid nix").unwrap(); + let path = CString::new("").unwrap(); + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), value); + assert_ne!(eval_err, nix_err_NIX_OK); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn realised_string_and_gc() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // String value + let string_val = nix_alloc_value(ctx, state); + let s = CString::new("hello world").unwrap(); + assert_eq!(nix_init_string(ctx, string_val, s.as_ptr()), nix_err_NIX_OK); + + // Realise string + let realised = nix_string_realise(ctx, state, string_val, false); + assert!(!realised.is_null()); + let buf = nix_realised_string_get_buffer_start(realised); + let len = nix_realised_string_get_buffer_size(realised); + let realised_str = + std::str::from_utf8(std::slice::from_raw_parts(buf.cast::(), len)).unwrap(); + assert_eq!(realised_str, "hello world"); + assert_eq!(nix_realised_string_get_store_path_count(realised), 0); + + nix_realised_string_free(realised); + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn big_thunk_evaluation() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + assert_eq!(nix_eval_state_builder_load(ctx, builder), nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a complex expression with lazy evaluation + let expr = + CString::new("let x = 1 + 2; y = x * 3; in { result = y + 4; other = x; }").unwrap(); + let path = CString::new("").unwrap(); + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // The top-level should be an attrset + assert_eq!(nix_get_type(ctx, value), ValueType_NIX_TYPE_ATTRS); + + // Get "result" attribute (ts should be a thunk initially) + let result_name = CString::new("result").unwrap(); + let result_val = nix_get_attr_byname(ctx, value, state, result_name.as_ptr()); + assert!(!result_val.is_null()); + + // Force the result + let force_err = nix_value_force(ctx, state, result_val); + assert_eq!(force_err, nix_err_NIX_OK); + + assert_eq!(nix_get_type(ctx, result_val), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, result_val), 13); // ((1+2)*3)+4 = 13 + + // Get "other" attribute + let other_name = CString::new("other").unwrap(); + let other_val = nix_get_attr_byname(ctx, value, state, other_name.as_ptr()); + assert!(!other_val.is_null()); + + let force_err2 = nix_value_force(ctx, state, other_val); + assert_eq!(force_err2, nix_err_NIX_OK); + + assert_eq!(nix_get_type(ctx, other_val), ValueType_NIX_TYPE_INT); + assert_eq!(nix_get_int(ctx, other_val), 3); // 1+2 = 3 + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn multi_argument_function_calls() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test evaluating a multi-argument function: (x: y: x + y) + let expr = CString::new("(x: y: x + y)").unwrap(); + let path = CString::new("/test").unwrap(); + + let func_value = nix_alloc_value(ctx, state); + assert!(!func_value.is_null()); + + let eval_err = + nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), func_value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // Force evaluation of the function + let force_err = nix_value_force(ctx, state, func_value); + assert_eq!(force_err, nix_err_NIX_OK); + + // Verify it's a function + let func_type = nix_get_type(ctx, func_value); + assert_eq!(func_type, ValueType_NIX_TYPE_FUNCTION); + + // Create arguments + let arg1 = nix_alloc_value(ctx, state); + let arg2 = nix_alloc_value(ctx, state); + assert!(!arg1.is_null() && !arg2.is_null()); + + let init_arg1_err = nix_init_int(ctx, arg1, 10); + let init_arg2_err = nix_init_int(ctx, arg2, 20); + assert_eq!(init_arg1_err, nix_err_NIX_OK); + assert_eq!(init_arg2_err, nix_err_NIX_OK); + + // Test multi-argument call using nix_value_call_multi + let mut args = [arg1, arg2]; + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = nix_value_call_multi(ctx, state, func_value, 2, args.as_mut_ptr(), result); + assert_eq!(call_err, nix_err_NIX_OK); + + // Force the result + let force_result_err = nix_value_force(ctx, state, result); + assert_eq!(force_result_err, nix_err_NIX_OK); + + // Check result type and value + let result_type = nix_get_type(ctx, result); + assert_eq!(result_type, ValueType_NIX_TYPE_INT); + + let result_value = nix_get_int(ctx, result); + assert_eq!(result_value, 30); // 10 + 20 + + // Clean up + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg2); + nix_value_decref(ctx, arg1); + nix_value_decref(ctx, func_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn curried_function_evaluation() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test evaluating a curried function: (x: y: z: x + y + z) + let expr = CString::new("(x: y: z: x + y + z)").unwrap(); + let path = CString::new("/test").unwrap(); + + let func_value = nix_alloc_value(ctx, state); + assert!(!func_value.is_null()); + + let eval_err = + nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), func_value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // Create three arguments + let arg1 = nix_alloc_value(ctx, state); + let arg2 = nix_alloc_value(ctx, state); + let arg3 = nix_alloc_value(ctx, state); + assert!(!arg1.is_null() && !arg2.is_null() && !arg3.is_null()); + + let _ = nix_init_int(ctx, arg1, 5); + let _ = nix_init_int(ctx, arg2, 10); + let _ = nix_init_int(ctx, arg3, 15); + + // Test calling with multiple arguments at once + let mut args = [arg1, arg2, arg3]; + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = nix_value_call_multi(ctx, state, func_value, 3, args.as_mut_ptr(), result); + assert_eq!(call_err, nix_err_NIX_OK); + + // Force the result + let force_result_err = nix_value_force(ctx, state, result); + assert_eq!(force_result_err, nix_err_NIX_OK); + + // Check result + let result_type = nix_get_type(ctx, result); + assert_eq!(result_type, ValueType_NIX_TYPE_INT); + + let result_value = nix_get_int(ctx, result); + assert_eq!(result_value, 30); // 5 + 10 + 15 + + // Test partial application using single calls + let partial1 = nix_alloc_value(ctx, state); + assert!(!partial1.is_null()); + + let partial_call1_err = nix_value_call(ctx, state, func_value, arg1, partial1); + assert_eq!(partial_call1_err, nix_err_NIX_OK); + + // partial1 should still be a function + let force_partial1_err = nix_value_force(ctx, state, partial1); + assert_eq!(force_partial1_err, nix_err_NIX_OK); + + let partial1_type = nix_get_type(ctx, partial1); + assert_eq!(partial1_type, ValueType_NIX_TYPE_FUNCTION); + + // Apply second argument + let partial2 = nix_alloc_value(ctx, state); + assert!(!partial2.is_null()); + + let partial_call2_err = nix_value_call(ctx, state, partial1, arg2, partial2); + assert_eq!(partial_call2_err, nix_err_NIX_OK); + + // partial2 should still be a function + let force_partial2_err = nix_value_force(ctx, state, partial2); + assert_eq!(force_partial2_err, nix_err_NIX_OK); + + let partial2_type = nix_get_type(ctx, partial2); + assert_eq!(partial2_type, ValueType_NIX_TYPE_FUNCTION); + + // Apply final argument + let final_result = nix_alloc_value(ctx, state); + assert!(!final_result.is_null()); + + let final_call_err = nix_value_call(ctx, state, partial2, arg3, final_result); + assert_eq!(final_call_err, nix_err_NIX_OK); + + // Force and check final result + let force_final_err = nix_value_force(ctx, state, final_result); + assert_eq!(force_final_err, nix_err_NIX_OK); + + let final_type = nix_get_type(ctx, final_result); + assert_eq!(final_type, ValueType_NIX_TYPE_INT); + + let final_value = nix_get_int(ctx, final_result); + assert_eq!(final_value, 30); // same result as multi-arg call + + // Clean up + nix_value_decref(ctx, final_result); + nix_value_decref(ctx, partial2); + nix_value_decref(ctx, partial1); + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg3); + nix_value_decref(ctx, arg2); + nix_value_decref(ctx, arg1); + nix_value_decref(ctx, func_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn thunk_creation_with_init_apply() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a simple function + let func_expr = CString::new("(x: x * 2)").unwrap(); + let path = CString::new("/test").unwrap(); + + let func_value = nix_alloc_value(ctx, state); + assert!(!func_value.is_null()); + + let eval_err = + nix_expr_eval_from_string(ctx, state, func_expr.as_ptr(), path.as_ptr(), func_value); + assert_eq!(eval_err, nix_err_NIX_OK); + + // Create an argument + let arg = nix_alloc_value(ctx, state); + assert!(!arg.is_null()); + + let init_arg_err = nix_init_int(ctx, arg, 21); + assert_eq!(init_arg_err, nix_err_NIX_OK); + + // Create a thunk using nix_init_apply (lazy evaluation) + let thunk = nix_alloc_value(ctx, state); + assert!(!thunk.is_null()); + + let apply_err = nix_init_apply(ctx, thunk, func_value, arg); + assert_eq!(apply_err, nix_err_NIX_OK); + + // Initially, the thunk should be of type THUNK + let thunk_type = nix_get_type(ctx, thunk); + assert_eq!(thunk_type, ValueType_NIX_TYPE_THUNK); + + // Force evaluation of the thunk + let force_err = nix_value_force(ctx, state, thunk); + assert_eq!(force_err, nix_err_NIX_OK); + + // After forcing, it should be an integer + let forced_type = nix_get_type(ctx, thunk); + assert_eq!(forced_type, ValueType_NIX_TYPE_INT); + + let result_value = nix_get_int(ctx, thunk); + assert_eq!(result_value, 42); // 21 * 2 + + // Clean up + nix_value_decref(ctx, thunk); + nix_value_decref(ctx, arg); + nix_value_decref(ctx, func_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn lookup_path_configuration() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + // Configure custom lookup path (NIX_PATH equivalent) + let lookup_paths = [ + CString::new("nixpkgs=/fake/nixpkgs").unwrap(), + CString::new("custom=/fake/custom").unwrap(), + ]; + + let lookup_path_ptrs: Vec<*const _> = lookup_paths.iter().map(|s| s.as_ptr()).collect(); + let mut lookup_path_ptrs_null_terminated = lookup_path_ptrs; + lookup_path_ptrs_null_terminated.push(std::ptr::null()); + + let set_lookup_err = nix_eval_state_builder_set_lookup_path( + ctx, + builder, + lookup_path_ptrs_null_terminated.as_mut_ptr(), + ); + assert_eq!(set_lookup_err, nix_err_NIX_OK); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Try to evaluate an expression that uses the lookup path + // NOTE: This will likely fail since the paths don't exist, but it tests the + // API + let expr = CString::new("builtins.nixPath").unwrap(); + let path = CString::new("/test").unwrap(); + + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), result); + + // The evaluation might succeed or fail depending on Nix version and + // configuration The important thing is that setting the lookup path + // didn't crash + if eval_err == nix_err_NIX_OK { + let force_err = nix_value_force(ctx, state, result); + if force_err == nix_err_NIX_OK { + let result_type = nix_get_type(ctx, result); + // nixPath should be a list + assert_eq!(result_type, ValueType_NIX_TYPE_LIST); + } + } + + // Clean up + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn complex_nested_evaluation() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Evaluate a simple nested expression + let expr = CString::new( + r#" + let + add = x: y: x + y; + data = { + values = [1 2 3 4 5]; + }; + in + { + original = data.values; + sum = builtins.foldl' add 0 data.values; + } + "#, + ) + .unwrap(); + let path = CString::new("/test").unwrap(); + + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let eval_err = nix_expr_eval_from_string(ctx, state, expr.as_ptr(), path.as_ptr(), result); + + // Complex expressions may fail sometimes, check for both success + // and error + if eval_err != nix_err_NIX_OK { + // If evaluation fails, skip the rest of the test + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + return; + } + + // Force deep evaluation + let force_err = nix_value_force_deep(ctx, state, result); + if force_err != nix_err_NIX_OK { + // If forcing fails, skip the rest of the test + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + return; + } + + // Verify result structure + let result_type = nix_get_type(ctx, result); + assert_eq!(result_type, ValueType_NIX_TYPE_ATTRS); + + let attrs_size = nix_get_attrs_size(ctx, result); + assert_eq!(attrs_size, 2); // original, sum + + // Check 'sum' attribute + let sum_key = CString::new("sum").unwrap(); + let sum_value = nix_get_attr_byname(ctx, result, state, sum_key.as_ptr()); + assert!(!sum_value.is_null()); + + let sum_type = nix_get_type(ctx, sum_value); + assert_eq!(sum_type, ValueType_NIX_TYPE_INT); + + let sum_result = nix_get_int(ctx, sum_value); + assert_eq!(sum_result, 15); // 1 + 2 + 3 + 4 + 5 + + // Check 'original' attribute (should be a list) + let original_key = CString::new("original").unwrap(); + let original_value = nix_get_attr_byname(ctx, result, state, original_key.as_ptr()); + if !original_value.is_null() { + let original_type = nix_get_type(ctx, original_value); + assert_eq!(original_type, ValueType_NIX_TYPE_LIST); + + let original_size = nix_get_list_size(ctx, original_value); + assert_eq!(original_size, 5); + + // Check first element of original list + let first_elem = nix_get_list_byidx(ctx, original_value, state, 0); + if !first_elem.is_null() { + let first_elem_type = nix_get_type(ctx, first_elem); + assert_eq!(first_elem_type, ValueType_NIX_TYPE_INT); + + let first_elem_value = nix_get_int(ctx, first_elem); + assert_eq!(first_elem_value, 1); + } + } + + // Clean up + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn evaluation_error_handling() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test evaluation with syntax error + let invalid_expr = CString::new("{ invalid syntax ").unwrap(); + let path = CString::new("/test").unwrap(); + + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let eval_err = + nix_expr_eval_from_string(ctx, state, invalid_expr.as_ptr(), path.as_ptr(), result); + assert_ne!(eval_err, nix_err_NIX_OK); // should fail + + // Clear error for next test + nix_clear_err(ctx); + + // Test evaluation with runtime error + let runtime_error_expr = CString::new("1 + \"string\"").unwrap(); + + let result2 = nix_alloc_value(ctx, state); + assert!(!result2.is_null()); + + let eval_err2 = nix_expr_eval_from_string( + ctx, + state, + runtime_error_expr.as_ptr(), + path.as_ptr(), + result2, + ); + + // May succeed at parse time but fail during evaluation + if eval_err2 == nix_err_NIX_OK { + let force_err = nix_value_force(ctx, state, result2); + assert_ne!(force_err, nix_err_NIX_OK); // should fail during forcing + } + + // Test error information retrieval + let error_code = nix_err_code(ctx); + assert_ne!(error_code, nix_err_NIX_OK); + + // Try to get error message + let mut error_len: std::os::raw::c_uint = 0; + let error_msg_ptr = nix_err_msg(ctx, ctx, &mut error_len as *mut _); + if !error_msg_ptr.is_null() && error_len > 0 { + let error_msg = std::str::from_utf8(std::slice::from_raw_parts( + error_msg_ptr as *const u8, + error_len as usize, + )) + .unwrap_or(""); + // Should contain some error information + assert!(!error_msg.is_empty()); + } + + // Test multi-argument call with wrong number of arguments + nix_clear_err(ctx); + + let func_expr = CString::new("(x: y: x + y)").unwrap(); + let func_value = nix_alloc_value(ctx, state); + assert!(!func_value.is_null()); + + let eval_func_err = + nix_expr_eval_from_string(ctx, state, func_expr.as_ptr(), path.as_ptr(), func_value); + assert_eq!(eval_func_err, nix_err_NIX_OK); + + // Try to call with wrong number of arguments. + // The function expects 2, but we give 1 + let arg = nix_alloc_value(ctx, state); + assert!(!arg.is_null()); + let _ = nix_init_int(ctx, arg, 5); + + let mut args = [arg]; + let result3 = nix_alloc_value(ctx, state); + assert!(!result3.is_null()); + + let call_err = nix_value_call_multi( + ctx, + state, + func_value, + 1, // only 1 argument, but function expects 2 + args.as_mut_ptr(), + result3, + ); + + // This should succeed but result should be a partially applied function + if call_err == nix_err_NIX_OK { + let force_err = nix_value_force(ctx, state, result3); + assert_eq!(force_err, nix_err_NIX_OK); + + let result_type = nix_get_type(ctx, result3); + assert_eq!(result_type, ValueType_NIX_TYPE_FUNCTION); // partially applied + } + + // Clean up + nix_value_decref(ctx, result3); + nix_value_decref(ctx, arg); + nix_value_decref(ctx, func_value); + nix_value_decref(ctx, result2); + nix_value_decref(ctx, result); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn builtin_function_calls() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test calling builtins.length + let length_expr = CString::new("builtins.length").unwrap(); + let path = CString::new("/test").unwrap(); + + let length_func = nix_alloc_value(ctx, state); + assert!(!length_func.is_null()); + + let eval_length_err = + nix_expr_eval_from_string(ctx, state, length_expr.as_ptr(), path.as_ptr(), length_func); + assert_eq!(eval_length_err, nix_err_NIX_OK); + + // Create a list to test with + let list_expr = CString::new("[1 2 3 4 5]").unwrap(); + let test_list = nix_alloc_value(ctx, state); + assert!(!test_list.is_null()); + + let eval_list_err = + nix_expr_eval_from_string(ctx, state, list_expr.as_ptr(), path.as_ptr(), test_list); + assert_eq!(eval_list_err, nix_err_NIX_OK); + + // Call length function with the list + let length_result = nix_alloc_value(ctx, state); + assert!(!length_result.is_null()); + + let call_length_err = nix_value_call(ctx, state, length_func, test_list, length_result); + assert_eq!(call_length_err, nix_err_NIX_OK); + + let force_length_err = nix_value_force(ctx, state, length_result); + assert_eq!(force_length_err, nix_err_NIX_OK); + + let length_type = nix_get_type(ctx, length_result); + assert_eq!(length_type, ValueType_NIX_TYPE_INT); + + let length_value = nix_get_int(ctx, length_result); + assert_eq!(length_value, 5); + + // Test builtins.map with multi-argument call + let map_expr = CString::new("builtins.map").unwrap(); + let map_func = nix_alloc_value(ctx, state); + assert!(!map_func.is_null()); + + let eval_map_err = + nix_expr_eval_from_string(ctx, state, map_expr.as_ptr(), path.as_ptr(), map_func); + assert_eq!(eval_map_err, nix_err_NIX_OK); + + // Create a simple function to map: (x: x * 2) + let double_expr = CString::new("(x: x * 2)").unwrap(); + let double_func = nix_alloc_value(ctx, state); + assert!(!double_func.is_null()); + + let eval_double_err = + nix_expr_eval_from_string(ctx, state, double_expr.as_ptr(), path.as_ptr(), double_func); + assert_eq!(eval_double_err, nix_err_NIX_OK); + + // Call map with the function and list + let mut args = [double_func, test_list]; + let map_result = nix_alloc_value(ctx, state); + assert!(!map_result.is_null()); + + let call_map_err = + nix_value_call_multi(ctx, state, map_func, 2, args.as_mut_ptr(), map_result); + assert_eq!(call_map_err, nix_err_NIX_OK); + + let force_map_err = nix_value_force(ctx, state, map_result); + assert_eq!(force_map_err, nix_err_NIX_OK); + + let map_result_type = nix_get_type(ctx, map_result); + assert_eq!(map_result_type, ValueType_NIX_TYPE_LIST); + + let map_result_size = nix_get_list_size(ctx, map_result); + assert_eq!(map_result_size, 5); + + // Check first element of mapped list (should be 2) + let first_mapped = nix_get_list_byidx(ctx, map_result, state, 0); + assert!(!first_mapped.is_null()); + + let force_first_err = nix_value_force(ctx, state, first_mapped); + assert_eq!(force_first_err, nix_err_NIX_OK); + + let first_mapped_type = nix_get_type(ctx, first_mapped); + assert_eq!(first_mapped_type, ValueType_NIX_TYPE_INT); + + let first_mapped_value = nix_get_int(ctx, first_mapped); + assert_eq!(first_mapped_value, 2); // 1 * 2 + + // Clean up + nix_value_decref(ctx, map_result); + nix_value_decref(ctx, double_func); + nix_value_decref(ctx, map_func); + nix_value_decref(ctx, length_result); + nix_value_decref(ctx, test_list); + nix_value_decref(ctx, length_func); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nixide-sys/tests/flake.rs b/nixide-sys/tests/flake.rs new file mode 100644 index 0000000..01fed21 --- /dev/null +++ b/nixide-sys/tests/flake.rs @@ -0,0 +1,78 @@ +#![cfg(test)] + +use std::ptr; + +use nixide_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn flake_settings_new_and_free() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + // Create new flake settings + let settings = nix_flake_settings_new(ctx); + assert!(!settings.is_null(), "nix_flake_settings_new returned null"); + + // Free flake settings (should not crash) + nix_flake_settings_free(settings); + + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn flake_settings_add_to_eval_state_builder() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let settings = nix_flake_settings_new(ctx); + assert!(!settings.is_null(), "nix_flake_settings_new returned null"); + + // Add flake settings to eval state builder + let err = nix_flake_settings_add_to_eval_state_builder(ctx, settings, builder); + // Accept OK or ERR_UNKNOWN (depends on Nix build/config) + assert!( + err == nix_err_NIX_OK || err == nix_err_NIX_ERR_UNKNOWN, + "nix_flake_settings_add_to_eval_state_builder returned unexpected error \ + code: {err}" + ); + + nix_flake_settings_free(settings); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn flake_settings_null_context() { + // Passing NULL context should not crash, but may error + unsafe { + let settings = nix_flake_settings_new(ptr::null_mut()); + // May return null if context is required + if !settings.is_null() { + nix_flake_settings_free(settings); + } + } +} diff --git a/nixide-sys/tests/memory.rs b/nixide-sys/tests/memory.rs new file mode 100644 index 0000000..229b6bd --- /dev/null +++ b/nixide-sys/tests/memory.rs @@ -0,0 +1,503 @@ +#![cfg(test)] + +use std::ffi::CString; + +use nixide_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn value_reference_counting() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a value + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + // Initialize with an integer + let init_err = nix_init_int(ctx, value, 42); + assert_eq!(init_err, nix_err_NIX_OK); + + // Test value-specific reference counting + let incref_err = nix_value_incref(ctx, value); + assert_eq!(incref_err, nix_err_NIX_OK); + + // Value should still be valid after increment + let int_val = nix_get_int(ctx, value); + assert_eq!(int_val, 42); + + // Test decrement + let decref_err = nix_value_decref(ctx, value); + assert_eq!(decref_err, nix_err_NIX_OK); + + // Value should still be valid (original reference still exists) + let int_val2 = nix_get_int(ctx, value); + assert_eq!(int_val2, 42); + + // Final decrement (should not crash) + let final_decref_err = nix_value_decref(ctx, value); + assert_eq!(final_decref_err, nix_err_NIX_OK); + + // Clean up + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn general_gc_reference_counting() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a value for general GC testing + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + let init_err = nix_init_string( + ctx, + value, + CString::new("test string for GC").unwrap().as_ptr(), + ); + assert_eq!(init_err, nix_err_NIX_OK); + + // Test general GC reference counting + let gc_incref_err = nix_gc_incref(ctx, value as *const ::std::os::raw::c_void); + assert_eq!(gc_incref_err, nix_err_NIX_OK); + + // Value should still be accessible + let value_type = nix_get_type(ctx, value); + assert_eq!(value_type, ValueType_NIX_TYPE_STRING); + + // Test GC decrement + let gc_decref_err = nix_gc_decref(ctx, value as *const ::std::os::raw::c_void); + assert_eq!(gc_decref_err, nix_err_NIX_OK); + + // Final cleanup + nix_value_decref(ctx, value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn manual_garbage_collection() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a few values to test basic GC functionality + let mut values = Vec::new(); + for i in 0..3 { + let value = nix_alloc_value(ctx, state); + if !value.is_null() { + let init_err = nix_init_int(ctx, value, i); + if init_err == nix_err_NIX_OK { + values.push(value); + } + } + } + + // Verify values are accessible before GC + for (i, &value) in values.iter().enumerate() { + let int_val = nix_get_int(ctx, value); + assert_eq!(int_val, i as i64); + } + + // Clean up values before attempting GC to avoid signal issues + for value in values { + nix_value_decref(ctx, value); + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn value_copying_and_memory_management() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create original value + let original = nix_alloc_value(ctx, state); + assert!(!original.is_null()); + + let test_string = CString::new("test string for copying").unwrap(); + let init_err = nix_init_string(ctx, original, test_string.as_ptr()); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create copy + let copy = nix_alloc_value(ctx, state); + assert!(!copy.is_null()); + + let copy_err = nix_copy_value(ctx, copy, original); + assert_eq!(copy_err, nix_err_NIX_OK); + + // Verify copy has same type and can be accessed + let original_type = nix_get_type(ctx, original); + let copy_type = nix_get_type(ctx, copy); + assert_eq!(original_type, copy_type); + assert_eq!(copy_type, ValueType_NIX_TYPE_STRING); + + // Test string contents using callback + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut original_string: Option = None; + let mut copy_string: Option = None; + + let _ = nix_get_string( + ctx, + original, + Some(string_callback), + &mut original_string as *mut Option as *mut ::std::os::raw::c_void, + ); + + let _ = nix_get_string( + ctx, + copy, + Some(string_callback), + &mut copy_string as *mut Option as *mut ::std::os::raw::c_void, + ); + + // Both should have the same string content + assert_eq!(original_string, copy_string); + assert!(original_string + .as_deref() + .unwrap_or("") + .contains("test string")); + + // Test reference counting on both values + let incref_orig = nix_value_incref(ctx, original); + let incref_copy = nix_value_incref(ctx, copy); + assert_eq!(incref_orig, nix_err_NIX_OK); + assert_eq!(incref_copy, nix_err_NIX_OK); + + // Values should still be accessible after increment + assert_eq!(nix_get_type(ctx, original), ValueType_NIX_TYPE_STRING); + assert_eq!(nix_get_type(ctx, copy), ValueType_NIX_TYPE_STRING); + + // Clean up with decrements + nix_value_decref(ctx, original); + nix_value_decref(ctx, original); // extra decref from incref + nix_value_decref(ctx, copy); + nix_value_decref(ctx, copy); // extra decref from incref + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn complex_value_memory_management() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a complex structure: list containing attribute sets + let list_builder = nix_make_list_builder(ctx, state, 2); + assert!(!list_builder.is_null()); + + // Create first element: attribute set + let attrs1 = nix_alloc_value(ctx, state); + assert!(!attrs1.is_null()); + + let bindings_builder1 = nix_make_bindings_builder(ctx, state, 2); + assert!(!bindings_builder1.is_null()); + + // Add attributes to first set + let key1 = CString::new("name").unwrap(); + let val1 = nix_alloc_value(ctx, state); + assert!(!val1.is_null()); + let name_str = CString::new("first").unwrap(); + let _ = nix_init_string(ctx, val1, name_str.as_ptr()); + + let insert_err1 = nix_bindings_builder_insert(ctx, bindings_builder1, key1.as_ptr(), val1); + assert_eq!(insert_err1, nix_err_NIX_OK); + + let key2 = CString::new("value").unwrap(); + let val2 = nix_alloc_value(ctx, state); + assert!(!val2.is_null()); + let _ = nix_init_int(ctx, val2, 42); + + let insert_err2 = nix_bindings_builder_insert(ctx, bindings_builder1, key2.as_ptr(), val2); + assert_eq!(insert_err2, nix_err_NIX_OK); + + let make_attrs_err1 = nix_make_attrs(ctx, attrs1, bindings_builder1); + assert_eq!(make_attrs_err1, nix_err_NIX_OK); + + // Insert first attrs into list + let list_insert_err1 = nix_list_builder_insert(ctx, list_builder, 0, attrs1); + assert_eq!(list_insert_err1, nix_err_NIX_OK); + + // Create second element + let attrs2 = nix_alloc_value(ctx, state); + assert!(!attrs2.is_null()); + + let bindings_builder2 = nix_make_bindings_builder(ctx, state, 1); + assert!(!bindings_builder2.is_null()); + + let key3 = CString::new("data").unwrap(); + let val3 = nix_alloc_value(ctx, state); + assert!(!val3.is_null()); + let data_str = CString::new("second").unwrap(); + let _ = nix_init_string(ctx, val3, data_str.as_ptr()); + + let insert_err3 = nix_bindings_builder_insert(ctx, bindings_builder2, key3.as_ptr(), val3); + assert_eq!(insert_err3, nix_err_NIX_OK); + + let make_attrs_err2 = nix_make_attrs(ctx, attrs2, bindings_builder2); + assert_eq!(make_attrs_err2, nix_err_NIX_OK); + + let list_insert_err2 = nix_list_builder_insert(ctx, list_builder, 1, attrs2); + assert_eq!(list_insert_err2, nix_err_NIX_OK); + + // Create final list + let final_list = nix_alloc_value(ctx, state); + assert!(!final_list.is_null()); + + let make_list_err = nix_make_list(ctx, list_builder, final_list); + assert_eq!(make_list_err, nix_err_NIX_OK); + + // Test the complex structure + assert_eq!(nix_get_type(ctx, final_list), ValueType_NIX_TYPE_LIST); + assert_eq!(nix_get_list_size(ctx, final_list), 2); + + // Access nested elements + let elem0 = nix_get_list_byidx(ctx, final_list, state, 0); + let elem1 = nix_get_list_byidx(ctx, final_list, state, 1); + assert!(!elem0.is_null() && !elem1.is_null()); + + assert_eq!(nix_get_type(ctx, elem0), ValueType_NIX_TYPE_ATTRS); + assert_eq!(nix_get_type(ctx, elem1), ValueType_NIX_TYPE_ATTRS); + + // Test memory management with deep copying + let copied_list = nix_alloc_value(ctx, state); + assert!(!copied_list.is_null()); + + let copy_err = nix_copy_value(ctx, copied_list, final_list); + assert_eq!(copy_err, nix_err_NIX_OK); + + // Force deep evaluation on copy + let deep_force_err = nix_value_force_deep(ctx, state, copied_list); + assert_eq!(deep_force_err, nix_err_NIX_OK); + + // Both should still be accessible + assert_eq!(nix_get_list_size(ctx, final_list), 2); + assert_eq!(nix_get_list_size(ctx, copied_list), 2); + + // Clean up all the values + nix_value_decref(ctx, copied_list); + nix_value_decref(ctx, final_list); + nix_value_decref(ctx, attrs2); + nix_value_decref(ctx, attrs1); + nix_value_decref(ctx, val3); + nix_value_decref(ctx, val2); + nix_value_decref(ctx, val1); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn memory_management_error_conditions() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + // Test reference counting with NULL pointers (should handle gracefully) + let null_incref_err = nix_gc_incref(ctx, std::ptr::null() as *const ::std::os::raw::c_void); + + // XXX: May succeed or fail depending on implementation. We can't really + // know, so assert both. + assert!(null_incref_err == nix_err_NIX_OK || null_incref_err == nix_err_NIX_ERR_UNKNOWN); + + let null_decref_err = nix_gc_decref(ctx, std::ptr::null() as *const ::std::os::raw::c_void); + assert!(null_decref_err == nix_err_NIX_OK || null_decref_err == nix_err_NIX_ERR_UNKNOWN); + + let null_value_incref_err = nix_value_incref(ctx, std::ptr::null_mut()); + // Some Nix APIs gracefully handle null pointers and return OK + assert!( + null_value_incref_err == nix_err_NIX_OK + || null_value_incref_err == nix_err_NIX_ERR_UNKNOWN + ); + + let null_value_decref_err = nix_value_decref(ctx, std::ptr::null_mut()); + // Some Nix APIs gracefully handle null pointers and return OK + assert!( + null_value_decref_err == nix_err_NIX_OK + || null_value_decref_err == nix_err_NIX_ERR_UNKNOWN + ); + + // Test copy with NULL values + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + let valid_value = nix_alloc_value(ctx, state); + assert!(!valid_value.is_null()); + + // Test copying to/from NULL + let copy_from_null_err = nix_copy_value(ctx, valid_value, std::ptr::null_mut()); + assert_ne!(copy_from_null_err, nix_err_NIX_OK); + + let copy_to_null_err = nix_copy_value(ctx, std::ptr::null_mut(), valid_value); + assert_ne!(copy_to_null_err, nix_err_NIX_OK); + + // Clean up + nix_value_decref(ctx, valid_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nixide-sys/tests/primop.rs b/nixide-sys/tests/primop.rs new file mode 100644 index 0000000..ecfacf8 --- /dev/null +++ b/nixide-sys/tests/primop.rs @@ -0,0 +1,639 @@ +#![cfg(test)] + +use std::{ + ffi::CString, + sync::atomic::{AtomicU32, Ordering}, +}; + +use nixide_sys::*; +use serial_test::serial; + +#[derive(Debug)] +struct TestPrimOpData { + call_count: AtomicU32, + last_arg_value: AtomicU32, +} + +// Simple PrimOp that adds 1 to an integer argument +unsafe extern "C" fn add_one_primop( + user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + state: *mut EvalState, + args: *mut *mut nix_value, + ret: *mut nix_value, +) { + if user_data.is_null() + || context.is_null() + || state.is_null() + || args.is_null() + || ret.is_null() + { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Null pointer in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + let data = unsafe { &*(user_data as *const TestPrimOpData) }; + data.call_count.fetch_add(1, Ordering::SeqCst); + + // Get first argument + let arg = unsafe { *args.offset(0) }; + if arg.is_null() { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Missing argument in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Force evaluation of argument + if unsafe { nix_value_force(context, state, arg) } != nix_err_NIX_OK { + return; + } + + // Check if argument is integer + if unsafe { nix_get_type(context, arg) } != ValueType_NIX_TYPE_INT { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Expected integer argument in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Get integer value and add 1 + let value = unsafe { nix_get_int(context, arg) }; + data.last_arg_value.store(value as u32, Ordering::SeqCst); + + // Set return value + let _ = unsafe { nix_init_int(context, ret, value + 1) }; +} + +// PrimOp that returns a constant string +unsafe extern "C" fn hello_world_primop( + _user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + _state: *mut EvalState, + _args: *mut *mut nix_value, + ret: *mut nix_value, +) { + let hello = CString::new("Hello from Rust PrimOp!").unwrap(); + let _ = unsafe { nix_init_string(context, ret, hello.as_ptr()) }; +} + +// PrimOp that takes multiple arguments and concatenates them +unsafe extern "C" fn concat_strings_primop( + _user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + state: *mut EvalState, + args: *mut *mut nix_value, + ret: *mut nix_value, +) { + if context.is_null() || state.is_null() || args.is_null() || ret.is_null() { + return; + } + + // This PrimOp expects exactly 2 string arguments + let mut result = String::new(); + + for i in 0..2 { + let arg = unsafe { *args.offset(i) }; + if arg.is_null() { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Missing argument in concat_strings_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Force evaluation + if unsafe { nix_value_force(context, state, arg) } != nix_err_NIX_OK { + return; + } + + // Check if it's a string + if unsafe { nix_get_type(context, arg) } != ValueType_NIX_TYPE_STRING { + let _ = unsafe { + static ITEMS: &[u8] = b"Expected string argument in concat_strings_primop\0"; + nix_set_err_msg(context, nix_err_NIX_ERR_UNKNOWN, ITEMS.as_ptr() as *const _) + }; + return; + } + + // Get string value using callback + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut String) }; + result.push_str(s); + } + + let _ = unsafe { + nix_get_string( + context, + arg, + Some(string_callback), + &mut result as *mut String as *mut ::std::os::raw::c_void, + ) + }; + } + + let result_cstr = CString::new(result).unwrap_or_else(|_| CString::new("").unwrap()); + let _ = unsafe { nix_init_string(context, ret, result_cstr.as_ptr()) }; +} + +#[test] +#[serial] +fn primop_allocation_and_registration() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create argument names + let arg_names = [CString::new("x").unwrap()]; + let arg_name_ptrs: Vec<*const _> = arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("addOne").unwrap(); + let doc = CString::new("Add 1 to the argument").unwrap(); + + // Allocate PrimOp + let primop = nix_alloc_primop( + ctx, + Some(add_one_primop), + 1, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + test_data_ptr as *mut ::std::os::raw::c_void, + ); + + if !primop.is_null() { + // Register the PrimOp globally + let register_err = nix_register_primop(ctx, primop); + // Registration may fail in some environments, but allocation should work + assert!( + register_err == nix_err_NIX_OK || register_err == nix_err_NIX_ERR_UNKNOWN, + "Unexpected error code: {register_err}" + ); + + // Test using the PrimOp by creating a value with it + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Clean up value + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_function_call() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create simple hello world PrimOp (no arguments) + let name = CString::new("helloWorld").unwrap(); + let doc = CString::new("Returns hello world string").unwrap(); + let mut empty_args: Vec<*const ::std::os::raw::c_char> = vec![std::ptr::null()]; + + let hello_primop = nix_alloc_primop( + ctx, + Some(hello_world_primop), + 0, // arity + name.as_ptr(), + empty_args.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + if !hello_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, hello_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Call the PrimOp (no arguments) + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = nix_value_call(ctx, state, primop_value, std::ptr::null_mut(), result); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is a string + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_STRING { + // Get string value + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = + unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut string_result: Option = None; + let _ = nix_get_string( + ctx, + result, + Some(string_callback), + &mut string_result as *mut Option as *mut ::std::os::raw::c_void, + ); + + // Verify we got the expected string + assert!(string_result + .as_deref() + .unwrap_or("") + .contains("Hello from Rust")); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_with_arguments() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create add one PrimOp + let arg_names = [CString::new("x").unwrap()]; + let arg_name_ptrs: Vec<*const _> = arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("addOne").unwrap(); + let doc = CString::new("Add 1 to the argument").unwrap(); + + let add_primop = nix_alloc_primop( + ctx, + Some(add_one_primop), + 1, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + test_data_ptr as *mut ::std::os::raw::c_void, + ); + + if !add_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, add_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create an integer argument + let arg_value = nix_alloc_value(ctx, state); + assert!(!arg_value.is_null()); + + let init_arg_err = nix_init_int(ctx, arg_value, 42); + assert_eq!(init_arg_err, nix_err_NIX_OK); + + // Call the PrimOp with the argument + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = nix_value_call(ctx, state, primop_value, arg_value, result); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is an integer + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_INT { + let result_value = nix_get_int(ctx, result); + assert_eq!(result_value, 43); // 42 + 1 + + // Verify callback was called + let test_data_ref = &*test_data_ptr; + assert_eq!(test_data_ref.call_count.load(Ordering::SeqCst), 1); + assert_eq!(test_data_ref.last_arg_value.load(Ordering::SeqCst), 42); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg_value); + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_multi_argument() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create concat strings PrimOp + let arg_names = [CString::new("s1").unwrap(), CString::new("s2").unwrap()]; + let arg_name_ptrs: Vec<*const _> = arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("concatStrings").unwrap(); + let doc = CString::new("Concatenate two strings").unwrap(); + + let concat_primop = nix_alloc_primop( + ctx, + Some(concat_strings_primop), + 2, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + if !concat_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, concat_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create string arguments + let arg1 = nix_alloc_value(ctx, state); + let arg2 = nix_alloc_value(ctx, state); + assert!(!arg1.is_null() && !arg2.is_null()); + + let hello_cstr = CString::new("Hello, ").unwrap(); + let world_cstr = CString::new("World!").unwrap(); + + let init_arg1_err = nix_init_string(ctx, arg1, hello_cstr.as_ptr()); + let init_arg2_err = nix_init_string(ctx, arg2, world_cstr.as_ptr()); + assert_eq!(init_arg1_err, nix_err_NIX_OK); + assert_eq!(init_arg2_err, nix_err_NIX_OK); + + // Test multi-argument call using nix_value_call_multi + let mut args = [arg1, arg2]; + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = + nix_value_call_multi(ctx, state, primop_value, 2, args.as_mut_ptr(), result); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is a string + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_STRING { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = + unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut string_result: Option = None; + let _ = nix_get_string( + ctx, + result, + Some(string_callback), + &mut string_result as *mut Option as *mut ::std::os::raw::c_void, + ); + + // Verify concatenation worked + assert_eq!(string_result.as_deref(), Some("Hello, World!")); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg2); + nix_value_decref(ctx, arg1); + nix_value_decref(ctx, primop_value); + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_error_handling() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test invalid PrimOp allocation (NULL callback) + let name = CString::new("invalid").unwrap(); + let doc = CString::new("Invalid PrimOp").unwrap(); + let mut empty_args: Vec<*const ::std::os::raw::c_char> = vec![std::ptr::null()]; + + let _invalid_primop = nix_alloc_primop( + ctx, + None, // NULL callback should cause error + 0, + name.as_ptr(), + empty_args.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + // Test initializing value with NULL PrimOp (should fail) + let test_value = nix_alloc_value(ctx, state); + assert!(!test_value.is_null()); + + nix_value_decref(ctx, test_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nixide-sys/tests/store.rs b/nixide-sys/tests/store.rs new file mode 100644 index 0000000..bb12a33 --- /dev/null +++ b/nixide-sys/tests/store.rs @@ -0,0 +1,278 @@ +#![cfg(test)] + +use std::{ffi::CString, ptr}; + +use nixide_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn libstore_init_and_open_free() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + // Open the default store (NULL URI, NULL params) + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + // Free the store and context + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn parse_and_clone_free_store_path() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + // Parse a store path (I'm using a dummy path, will likely be invalid but + // should not segfault) XXX: store_path may be null if path is invalid, + // but should not crash + let path_str = CString::new("/nix/store/dummy-path").unwrap(); + let store_path = nix_store_parse_path(ctx, store, path_str.as_ptr()); + + if !store_path.is_null() { + // Clone and free + let cloned = nix_store_path_clone(store_path); + assert!(!cloned.is_null()); + nix_store_path_free(cloned); + nix_store_path_free(store_path); + } + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_get_uri_and_storedir() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, ptr::null(), ptr::null_mut()); + assert!(!store.is_null()); + + let mut uri: Option = None; + let res = nix_store_get_uri(ctx, store, Some(string_callback), (&raw mut uri).cast()); + assert_eq!(res, nix_err_NIX_OK); + assert!(uri.is_some()); + + let mut storedir: Option = None; + let res = nix_store_get_storedir( + ctx, + store, + Some(string_callback), + (&raw mut storedir).cast(), + ); + assert_eq!(res, nix_err_NIX_OK); + assert!(storedir.is_some()); + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn libstore_init_no_load_config() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init_no_load_config(ctx); + assert_eq!(err, nix_err_NIX_OK); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_is_valid_path_and_real_path() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + // Use a dummy path (should not be valid, but should not crash) + let path_str = CString::new("/nix/store/dummy-path").unwrap(); + let store_path = nix_store_parse_path(ctx, store, path_str.as_ptr()); + if !store_path.is_null() { + let valid = nix_store_is_valid_path(ctx, store, store_path); + assert!(!valid, "Dummy path should not be valid"); + + let mut real_path: Option = None; + let res = nix_store_real_path( + ctx, + store, + store_path, + Some(string_callback), + (&raw mut real_path).cast(), + ); + // May fail, but should not crash + assert!(res == nix_err_NIX_OK || res == nix_err_NIX_ERR_UNKNOWN); + nix_store_path_free(store_path); + } + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_path_name() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let path_str = CString::new("/nix/store/foo-bar-baz").unwrap(); + let store_path = nix_store_parse_path(ctx, store, path_str.as_ptr()); + if !store_path.is_null() { + let mut name: Option = None; + nix_store_path_name(store_path, Some(string_callback), (&raw mut name).cast()); + // Should extract the name part ("foo-bar-baz") + assert!(name.as_deref().unwrap_or("").contains("foo-bar-baz")); + nix_store_path_free(store_path); + } + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_get_version() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + unsafe { *out = Some(s.to_string()) }; + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let mut version: Option = None; + let res = + nix_store_get_version(ctx, store, Some(string_callback), (&raw mut version).cast()); + assert_eq!(res, nix_err_NIX_OK); + // Version may be empty for dummy stores, but should not crash + assert!(version.is_some()); + + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn store_realise_and_copy_closure() { + unsafe extern "C" fn realise_callback( + _userdata: *mut ::std::os::raw::c_void, + outname: *const ::std::os::raw::c_char, + out: *const StorePath, + ) { + // Just check that callback is called with non-null pointers + assert!(!outname.is_null()); + assert!(!out.is_null()); + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + // Use a dummy path (should not crash, may not realise) + let path_str = CString::new("/nix/store/dummy-path").unwrap(); + let store_path = nix_store_parse_path(ctx, store, path_str.as_ptr()); + if !store_path.is_null() { + // Realise (should fail, but must not crash) + let _ = nix_store_realise( + ctx, + store, + store_path, + std::ptr::null_mut(), + Some(realise_callback), + ); + + // Copy closure to same store (should fail, but must not crash) + let _ = nix_store_copy_closure(ctx, store, store, store_path); + + nix_store_path_free(store_path); + } + + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nixide-sys/tests/util.rs b/nixide-sys/tests/util.rs new file mode 100644 index 0000000..038d30b --- /dev/null +++ b/nixide-sys/tests/util.rs @@ -0,0 +1,152 @@ +#![cfg(test)] + +use std::ffi::{CStr, CString}; + +use nixide_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn context_create_and_free() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn libutil_init() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn version_get() { + unsafe { + let version_ptr = nix_version_get(); + assert!(!version_ptr.is_null()); + let version = CStr::from_ptr(version_ptr).to_string_lossy(); + assert!(!version.is_empty(), "Version string should not be empty"); + assert!( + version.chars().next().unwrap().is_ascii_digit(), + "Version string should start with a digit: {version}" + ); + } +} + +#[test] +#[serial] +fn setting_set_and_get() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + *unsafe { &mut *out } = Some(s.to_string()); + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + // Set a setting (use a dummy/extra setting to avoid breaking global config) + let key = CString::new("extra-test-setting").unwrap(); + let value = CString::new("test-value").unwrap(); + let set_err = nix_setting_set(ctx, key.as_ptr(), value.as_ptr()); + // Setting may not exist, but should not crash + assert!( + set_err == nix_err_NIX_OK || set_err == nix_err_NIX_ERR_KEY, + "Unexpected error code: {set_err}" + ); + + // Try to get the setting (may not exist, but should not crash) + let mut got: Option = None; + let get_err = nix_setting_get( + ctx, + key.as_ptr(), + Some(string_callback), + (&raw mut got).cast(), + ); + assert!( + get_err == nix_err_NIX_OK || get_err == nix_err_NIX_ERR_KEY, + "Unexpected error code: {get_err}" + ); + // If OK, we should have gotten a value + if get_err == nix_err_NIX_OK { + assert_eq!(got.as_deref(), Some("test-value")); + } + + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn error_handling_apis() { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap(); + let out = user_data.cast::>(); + *unsafe { &mut *out } = Some(s.to_string()); + } + + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + // Set an error message + let msg = CString::new("custom error message").unwrap(); + let set_err = nix_set_err_msg(ctx, nix_err_NIX_ERR_UNKNOWN, msg.as_ptr()); + assert_eq!(set_err, nix_err_NIX_ERR_UNKNOWN); + + // Get error code + let code = nix_err_code(ctx); + assert_eq!(code, nix_err_NIX_ERR_UNKNOWN); + + // Get error message + let mut len: std::os::raw::c_uint = 0; + let err_msg_ptr = nix_err_msg(ctx, ctx, &mut len as *mut _); + if !err_msg_ptr.is_null() && len > 0 { + let err_msg = std::str::from_utf8(std::slice::from_raw_parts( + err_msg_ptr as *const u8, + len as usize, + )) + .unwrap(); + assert!(err_msg.contains("custom error message")); + } + + // Get error info message (should work, but may be empty) + let mut info: Option = None; + let _ = nix_err_info_msg(ctx, ctx, Some(string_callback), (&raw mut info).cast()); + + // Get error name (should work, but may be empty) + let mut name: Option = None; + let _ = nix_err_name(ctx, ctx, Some(string_callback), (&raw mut name).cast()); + + // Clear error + nix_clear_err(ctx); + let code_after_clear = nix_err_code(ctx); + assert_eq!(code_after_clear, nix_err_NIX_OK); + + nix_c_context_free(ctx); + } +}