nix-bindings 0.2347.4

Rust binding for Nix, the build tool
Documentation
use std::{ffi::CString, ptr::NonNull};

use crate::{Error, Result, Value};

impl Value<'_> {
  /// Get an attribute by name.
  ///
  /// Returns the value associated with the given attribute name.
  ///
  /// # Errors
  ///
  /// Returns an error if the value is not an attribute set or the key
  /// does not exist.
  pub fn get_attr(&self, key: &str) -> Result<Value<'_>> {
    if self.value_type() != crate::ValueType::Attrs {
      return Err(Error::InvalidType {
        expected: "attrs",
        actual:   self.value_type().to_string(),
      });
    }

    let key_c = CString::new(key)?;

    // SAFETY: context, value, and state are valid, type is checked
    // nix_get_attr_byname returns an owned (GC-reffed) pointer; we are
    // responsible for decref, which Value's Drop handles.
    let attr_ptr = unsafe {
      crate::sys::nix_get_attr_byname(
        self.state.context.as_ptr(),
        self.inner.as_ptr(),
        self.state.as_ptr(),
        key_c.as_ptr(),
      )
    };

    match NonNull::new(attr_ptr) {
      Some(inner) => {
        Ok(Value {
          inner,
          state: self.state,
        })
      },
      None => {
        // Distinguish missing-key (no error on context) from an actual API
        // failure that parked a message on the context.
        let ptr = unsafe {
          crate::sys::nix_err_msg(
            std::ptr::null_mut(),
            self.state.context.as_ptr(),
            std::ptr::null_mut(),
          )
        };
        if ptr.is_null() {
          Err(Error::KeyNotFound(key.to_string()))
        } else {
          let msg = unsafe {
            std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned()
          };
          if msg.is_empty() {
            Err(Error::KeyNotFound(key.to_string()))
          } else {
            Err(Error::Unknown(msg))
          }
        }
      },
    }
  }

  /// Get all attribute keys.
  ///
  /// Returns a vector of all attribute names in this attribute set.
  ///
  /// # Errors
  ///
  /// Returns an error if the value is not an attribute set.
  pub fn attr_keys(&self) -> Result<Vec<String>> {
    if self.value_type() != crate::ValueType::Attrs {
      return Err(Error::InvalidType {
        expected: "attrs",
        actual:   self.value_type().to_string(),
      });
    }

    // SAFETY: context and value are valid, type is checked
    let count = unsafe {
      crate::sys::nix_get_attrs_size(
        self.state.context.as_ptr(),
        self.inner.as_ptr(),
      )
    };

    let mut keys = Vec::with_capacity(count as usize);

    for i in 0..count {
      // nix_get_attr_name_byidx returns the name only. No GC ref on a
      // value side, no thunk forcing. Use it instead of nix_get_attr_byidx
      // which would alloc-and-decref a value we'd immediately discard.
      // SAFETY: context, value, and state are valid; index is in bounds.
      let name_ptr = unsafe {
        crate::sys::nix_get_attr_name_byidx(
          self.state.context.as_ptr(),
          self.inner.as_ptr(),
          self.state.as_ptr(),
          i,
        )
      };

      if name_ptr.is_null() {
        continue;
      }

      // SAFETY: name_ptr is a valid C string owned by the EvalState
      let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) }
        .to_str()
        .map_err(|_| {
          Error::Unknown("Attribute name was not valid UTF-8".to_string())
        })?
        .to_owned();
      keys.push(name);
    }

    Ok(keys)
  }

  /// Check if an attribute exists.
  ///
  /// Returns true if the attribute set contains the given key.
  ///
  /// # Errors
  ///
  /// Returns an error if the value is not an attribute set.
  pub fn has_attr(&self, key: &str) -> Result<bool> {
    if self.value_type() != crate::ValueType::Attrs {
      return Err(Error::InvalidType {
        expected: "attrs",
        actual:   self.value_type().to_string(),
      });
    }

    let key_c = CString::new(key)?;

    // SAFETY: context, value, and state are valid, type is checked
    let result = unsafe {
      crate::sys::nix_has_attr_byname(
        self.state.context.as_ptr(),
        self.inner.as_ptr(),
        self.state.as_ptr(),
        key_c.as_ptr(),
      )
    };

    Ok(result)
  }

  /// Create an iterator over key-value pairs.
  ///
  /// # Returns
  ///
  /// An iterator over all key-value pairs in the attribute set.
  ///
  /// # Errors
  ///
  /// Returns an error if the value is not an attribute set.
  pub fn attrs(&self) -> Result<AttrIterator<'_>> {
    if self.value_type() != crate::ValueType::Attrs {
      return Err(Error::InvalidType {
        expected: "attrs",
        actual:   self.value_type().to_string(),
      });
    }

    // SAFETY: context and value are valid, type is checked
    let count = unsafe {
      crate::sys::nix_get_attrs_size(
        self.state.context.as_ptr(),
        self.inner.as_ptr(),
      )
    };

    Ok(AttrIterator {
      value: self,
      index: 0,
      count: count as usize,
    })
  }
}

/// Iterator over attribute set key-value pairs.
///
/// This struct provides a way to iterate through all attributes
/// in a Nix attribute set, yielding both the key and value for
/// each attribute.
pub struct AttrIterator<'a> {
  value: &'a Value<'a>,
  index: usize,
  count: usize,
}

impl<'a> Iterator for AttrIterator<'a> {
  // Item lifetime is tied to the source Value's lifetime 'a, not 'static.
  type Item = Result<(String, Value<'a>)>;

  fn next(&mut self) -> Option<Self::Item> {
    if self.index >= self.count {
      return None;
    }

    let idx = self.index;
    self.index += 1;

    // SAFETY: context, value, and state are valid; index is in bounds.
    // nix_get_attr_byidx_lazy yields a GC-reffed pointer without forcing
    // the thunk; callers force on demand via the as_* accessors. Value's
    // Drop releases the ref.
    let mut name_ptr: *const std::os::raw::c_char = std::ptr::null();
    let attr_ptr = unsafe {
      crate::sys::nix_get_attr_byidx_lazy(
        self.value.state.context.as_ptr(),
        self.value.inner.as_ptr(),
        self.value.state.as_ptr(),
        idx as std::os::raw::c_uint,
        &mut name_ptr,
      )
    };

    if attr_ptr.is_null() {
      return Some(Err(Error::NullPointer));
    }

    if name_ptr.is_null() {
      // attr_ptr is GC-reffed; we must release it before returning.
      unsafe {
        crate::sys::nix_value_decref(
          self.value.state.context.as_ptr(),
          attr_ptr,
        );
      }
      return Some(Err(Error::NullPointer));
    }

    // SAFETY: name_ptr is a valid C string owned by the EvalState
    let name = match unsafe { std::ffi::CStr::from_ptr(name_ptr) }.to_str() {
      Ok(s) => s.to_owned(),
      Err(_) => {
        unsafe {
          crate::sys::nix_value_decref(
            self.value.state.context.as_ptr(),
            attr_ptr,
          );
        }
        return Some(Err(Error::Unknown(
          "Attribute name was not valid UTF-8".to_string(),
        )));
      },
    };

    // SAFETY: attr_ptr is non-null, verified above
    let inner = unsafe { NonNull::new_unchecked(attr_ptr) };

    let value = Value {
      inner,
      state: self.value.state,
    };

    Some(Ok((name, value)))
  }

  fn size_hint(&self) -> (usize, Option<usize>) {
    let remaining = self.count - self.index;
    (remaining, Some(remaining))
  }
}

impl ExactSizeIterator for AttrIterator<'_> {}

#[cfg(test)]
mod tests {
  use std::sync::Arc;

  use serial_test::serial;

  use crate::{Context, EvalStateBuilder, Store};

  fn setup() -> EvalStateBuilder {
    let ctx = Arc::new(Context::new().expect("Failed to create context"));
    let store =
      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
    EvalStateBuilder::new(&store).expect("Failed to create builder")
  }

  #[test]
  #[serial]
  fn test_get_attr() {
    let state = setup().build().expect("Failed to build state");
    let attrs = state
      .eval_from_string("{ foo = 42; bar = \"hello\"; }", "<eval>")
      .expect("Failed to evaluate attrs");

    let foo = attrs.get_attr("foo").expect("Failed to get foo");
    assert_eq!(foo.as_int().expect("Failed to get int"), 42);

    let bar = attrs.get_attr("bar").expect("Failed to get bar");
    assert_eq!(bar.as_string().expect("Failed to get string"), "hello");
  }

  #[test]
  #[serial]
  fn test_get_attr_missing() {
    let state = setup().build().expect("Failed to build state");
    let attrs = state
      .eval_from_string("{ foo = 42; }", "<eval>")
      .expect("Failed to evaluate attrs");

    let result = attrs.get_attr("missing");
    assert!(result.is_err());
  }

  #[test]
  #[serial]
  fn test_attr_keys() {
    let state = setup().build().expect("Failed to build state");
    let attrs = state
      .eval_from_string("{ foo = 1; bar = 2; baz = 3; }", "<eval>")
      .expect("Failed to evaluate attrs");

    let keys = attrs.attr_keys().expect("Failed to get keys");
    assert_eq!(keys.len(), 3);
    assert!(keys.contains(&"foo".to_string()));
    assert!(keys.contains(&"bar".to_string()));
    assert!(keys.contains(&"baz".to_string()));
  }

  #[test]
  #[serial]
  fn test_has_attr() {
    let state = setup().build().expect("Failed to build state");
    let attrs = state
      .eval_from_string("{ foo = 42; }", "<eval>")
      .expect("Failed to evaluate attrs");

    assert!(attrs.has_attr("foo").expect("Failed to check attr"));
    assert!(!attrs.has_attr("bar").expect("Failed to check missing attr"));
  }

  #[test]
  #[serial]
  fn test_attr_iterator() {
    let state = setup().build().expect("Failed to build state");
    let attrs = state
      .eval_from_string("{ a = 1; b = 2; c = 3; }", "<eval>")
      .expect("Failed to evaluate attrs");

    let iter = attrs.attrs().expect("Failed to create iterator");
    let collected: Vec<_> = iter.collect();

    assert_eq!(collected.len(), 3);
  }

  #[test]
  #[serial]
  fn test_empty_attrs() {
    let state = setup().build().expect("Failed to build state");
    let attrs = state
      .eval_from_string("{}", "<eval>")
      .expect("Failed to evaluate empty attrs");

    let keys = attrs.attr_keys().expect("Failed to get keys");
    assert!(keys.is_empty());

    let has = attrs.has_attr("foo").expect("Failed to check attr");
    assert!(!has);
  }
}