Skip to main content

nix_bindings/
attrs.rs

1use std::{ffi::CString, ptr::NonNull};
2
3use crate::{Error, Result, Value};
4
5impl Value<'_> {
6  /// Get an attribute by name.
7  ///
8  /// Returns the value associated with the given attribute name.
9  ///
10  /// # Errors
11  ///
12  /// Returns an error if the value is not an attribute set or the key
13  /// does not exist.
14  pub fn get_attr(&self, key: &str) -> Result<Value<'_>> {
15    if self.value_type() != crate::ValueType::Attrs {
16      return Err(Error::InvalidType {
17        expected: "attrs",
18        actual:   self.value_type().to_string(),
19      });
20    }
21
22    let key_c = CString::new(key)?;
23
24    // SAFETY: context, value, and state are valid, type is checked
25    // nix_get_attr_byname returns an owned (GC-reffed) pointer; we are
26    // responsible for decref, which Value's Drop handles.
27    let attr_ptr = unsafe {
28      crate::sys::nix_get_attr_byname(
29        self.state.context.as_ptr(),
30        self.inner.as_ptr(),
31        self.state.as_ptr(),
32        key_c.as_ptr(),
33      )
34    };
35
36    match NonNull::new(attr_ptr) {
37      Some(inner) => {
38        Ok(Value {
39          inner,
40          state: self.state,
41        })
42      },
43      None => {
44        // Distinguish missing-key (no error on context) from an actual API
45        // failure that parked a message on the context.
46        let ptr = unsafe {
47          crate::sys::nix_err_msg(
48            std::ptr::null_mut(),
49            self.state.context.as_ptr(),
50            std::ptr::null_mut(),
51          )
52        };
53        if ptr.is_null() {
54          Err(Error::KeyNotFound(key.to_string()))
55        } else {
56          let msg = unsafe {
57            std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned()
58          };
59          if msg.is_empty() {
60            Err(Error::KeyNotFound(key.to_string()))
61          } else {
62            Err(Error::Unknown(msg))
63          }
64        }
65      },
66    }
67  }
68
69  /// Get all attribute keys.
70  ///
71  /// Returns a vector of all attribute names in this attribute set.
72  ///
73  /// # Errors
74  ///
75  /// Returns an error if the value is not an attribute set.
76  pub fn attr_keys(&self) -> Result<Vec<String>> {
77    if self.value_type() != crate::ValueType::Attrs {
78      return Err(Error::InvalidType {
79        expected: "attrs",
80        actual:   self.value_type().to_string(),
81      });
82    }
83
84    // SAFETY: context and value are valid, type is checked
85    let count = unsafe {
86      crate::sys::nix_get_attrs_size(
87        self.state.context.as_ptr(),
88        self.inner.as_ptr(),
89      )
90    };
91
92    let mut keys = Vec::with_capacity(count as usize);
93
94    for i in 0..count {
95      // nix_get_attr_name_byidx returns the name only. No GC ref on a
96      // value side, no thunk forcing. Use it instead of nix_get_attr_byidx
97      // which would alloc-and-decref a value we'd immediately discard.
98      // SAFETY: context, value, and state are valid; index is in bounds.
99      let name_ptr = unsafe {
100        crate::sys::nix_get_attr_name_byidx(
101          self.state.context.as_ptr(),
102          self.inner.as_ptr(),
103          self.state.as_ptr(),
104          i,
105        )
106      };
107
108      if name_ptr.is_null() {
109        continue;
110      }
111
112      // SAFETY: name_ptr is a valid C string owned by the EvalState
113      let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) }
114        .to_str()
115        .map_err(|_| {
116          Error::Unknown("Attribute name was not valid UTF-8".to_string())
117        })?
118        .to_owned();
119      keys.push(name);
120    }
121
122    Ok(keys)
123  }
124
125  /// Check if an attribute exists.
126  ///
127  /// Returns true if the attribute set contains the given key.
128  ///
129  /// # Errors
130  ///
131  /// Returns an error if the value is not an attribute set.
132  pub fn has_attr(&self, key: &str) -> Result<bool> {
133    if self.value_type() != crate::ValueType::Attrs {
134      return Err(Error::InvalidType {
135        expected: "attrs",
136        actual:   self.value_type().to_string(),
137      });
138    }
139
140    let key_c = CString::new(key)?;
141
142    // SAFETY: context, value, and state are valid, type is checked
143    let result = unsafe {
144      crate::sys::nix_has_attr_byname(
145        self.state.context.as_ptr(),
146        self.inner.as_ptr(),
147        self.state.as_ptr(),
148        key_c.as_ptr(),
149      )
150    };
151
152    Ok(result)
153  }
154
155  /// Create an iterator over key-value pairs.
156  ///
157  /// # Returns
158  ///
159  /// An iterator over all key-value pairs in the attribute set.
160  ///
161  /// # Errors
162  ///
163  /// Returns an error if the value is not an attribute set.
164  pub fn attrs(&self) -> Result<AttrIterator<'_>> {
165    if self.value_type() != crate::ValueType::Attrs {
166      return Err(Error::InvalidType {
167        expected: "attrs",
168        actual:   self.value_type().to_string(),
169      });
170    }
171
172    // SAFETY: context and value are valid, type is checked
173    let count = unsafe {
174      crate::sys::nix_get_attrs_size(
175        self.state.context.as_ptr(),
176        self.inner.as_ptr(),
177      )
178    };
179
180    Ok(AttrIterator {
181      value: self,
182      index: 0,
183      count: count as usize,
184    })
185  }
186}
187
188/// Iterator over attribute set key-value pairs.
189///
190/// This struct provides a way to iterate through all attributes
191/// in a Nix attribute set, yielding both the key and value for
192/// each attribute.
193pub struct AttrIterator<'a> {
194  value: &'a Value<'a>,
195  index: usize,
196  count: usize,
197}
198
199impl<'a> Iterator for AttrIterator<'a> {
200  // Item lifetime is tied to the source Value's lifetime 'a, not 'static.
201  type Item = Result<(String, Value<'a>)>;
202
203  fn next(&mut self) -> Option<Self::Item> {
204    if self.index >= self.count {
205      return None;
206    }
207
208    let idx = self.index;
209    self.index += 1;
210
211    // SAFETY: context, value, and state are valid; index is in bounds.
212    // nix_get_attr_byidx_lazy yields a GC-reffed pointer without forcing
213    // the thunk; callers force on demand via the as_* accessors. Value's
214    // Drop releases the ref.
215    let mut name_ptr: *const std::os::raw::c_char = std::ptr::null();
216    let attr_ptr = unsafe {
217      crate::sys::nix_get_attr_byidx_lazy(
218        self.value.state.context.as_ptr(),
219        self.value.inner.as_ptr(),
220        self.value.state.as_ptr(),
221        idx as std::os::raw::c_uint,
222        &mut name_ptr,
223      )
224    };
225
226    if attr_ptr.is_null() {
227      return Some(Err(Error::NullPointer));
228    }
229
230    if name_ptr.is_null() {
231      // attr_ptr is GC-reffed; we must release it before returning.
232      unsafe {
233        crate::sys::nix_value_decref(
234          self.value.state.context.as_ptr(),
235          attr_ptr,
236        );
237      }
238      return Some(Err(Error::NullPointer));
239    }
240
241    // SAFETY: name_ptr is a valid C string owned by the EvalState
242    let name = match unsafe { std::ffi::CStr::from_ptr(name_ptr) }.to_str() {
243      Ok(s) => s.to_owned(),
244      Err(_) => {
245        unsafe {
246          crate::sys::nix_value_decref(
247            self.value.state.context.as_ptr(),
248            attr_ptr,
249          );
250        }
251        return Some(Err(Error::Unknown(
252          "Attribute name was not valid UTF-8".to_string(),
253        )));
254      },
255    };
256
257    // SAFETY: attr_ptr is non-null, verified above
258    let inner = unsafe { NonNull::new_unchecked(attr_ptr) };
259
260    let value = Value {
261      inner,
262      state: self.value.state,
263    };
264
265    Some(Ok((name, value)))
266  }
267
268  fn size_hint(&self) -> (usize, Option<usize>) {
269    let remaining = self.count - self.index;
270    (remaining, Some(remaining))
271  }
272}
273
274impl ExactSizeIterator for AttrIterator<'_> {}
275
276#[cfg(test)]
277mod tests {
278  use std::sync::Arc;
279
280  use serial_test::serial;
281
282  use crate::{Context, EvalStateBuilder, Store};
283
284  fn setup() -> EvalStateBuilder {
285    let ctx = Arc::new(Context::new().expect("Failed to create context"));
286    let store =
287      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
288    EvalStateBuilder::new(&store).expect("Failed to create builder")
289  }
290
291  #[test]
292  #[serial]
293  fn test_get_attr() {
294    let state = setup().build().expect("Failed to build state");
295    let attrs = state
296      .eval_from_string("{ foo = 42; bar = \"hello\"; }", "<eval>")
297      .expect("Failed to evaluate attrs");
298
299    let foo = attrs.get_attr("foo").expect("Failed to get foo");
300    assert_eq!(foo.as_int().expect("Failed to get int"), 42);
301
302    let bar = attrs.get_attr("bar").expect("Failed to get bar");
303    assert_eq!(bar.as_string().expect("Failed to get string"), "hello");
304  }
305
306  #[test]
307  #[serial]
308  fn test_get_attr_missing() {
309    let state = setup().build().expect("Failed to build state");
310    let attrs = state
311      .eval_from_string("{ foo = 42; }", "<eval>")
312      .expect("Failed to evaluate attrs");
313
314    let result = attrs.get_attr("missing");
315    assert!(result.is_err());
316  }
317
318  #[test]
319  #[serial]
320  fn test_attr_keys() {
321    let state = setup().build().expect("Failed to build state");
322    let attrs = state
323      .eval_from_string("{ foo = 1; bar = 2; baz = 3; }", "<eval>")
324      .expect("Failed to evaluate attrs");
325
326    let keys = attrs.attr_keys().expect("Failed to get keys");
327    assert_eq!(keys.len(), 3);
328    assert!(keys.contains(&"foo".to_string()));
329    assert!(keys.contains(&"bar".to_string()));
330    assert!(keys.contains(&"baz".to_string()));
331  }
332
333  #[test]
334  #[serial]
335  fn test_has_attr() {
336    let state = setup().build().expect("Failed to build state");
337    let attrs = state
338      .eval_from_string("{ foo = 42; }", "<eval>")
339      .expect("Failed to evaluate attrs");
340
341    assert!(attrs.has_attr("foo").expect("Failed to check attr"));
342    assert!(!attrs.has_attr("bar").expect("Failed to check missing attr"));
343  }
344
345  #[test]
346  #[serial]
347  fn test_attr_iterator() {
348    let state = setup().build().expect("Failed to build state");
349    let attrs = state
350      .eval_from_string("{ a = 1; b = 2; c = 3; }", "<eval>")
351      .expect("Failed to evaluate attrs");
352
353    let iter = attrs.attrs().expect("Failed to create iterator");
354    let collected: Vec<_> = iter.collect();
355
356    assert_eq!(collected.len(), 3);
357  }
358
359  #[test]
360  #[serial]
361  fn test_empty_attrs() {
362    let state = setup().build().expect("Failed to build state");
363    let attrs = state
364      .eval_from_string("{}", "<eval>")
365      .expect("Failed to evaluate empty attrs");
366
367    let keys = attrs.attr_keys().expect("Failed to get keys");
368    assert!(keys.is_empty());
369
370    let has = attrs.has_attr("foo").expect("Failed to check attr");
371    assert!(!has);
372  }
373}