Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions libuci-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,148 @@ mod bindings {
}
}
}

/// Casts a pointer `ptr` of a struct `container` member to the containing struct pointer.
///
/// # Safety
/// The caller must ensure that `$container.$field` has the same type as `$ptr` points to.
macro_rules! container_of {
($ptr:expr, $container:ty, $field:ident) => {{
$ptr.cast::<u8>()
.wrapping_sub(std::mem::offset_of!($container, $field))
.cast::<$container>()
}};
}

/// casts an uci_list pointer to the containing uci_element.
///
/// # Safety
/// The caller must ensure that `ptr` points to a list which is member of an uci_element.
/// The `ptr` must not point to a list which is not contained in an uci_element.
pub unsafe fn list_to_element(ptr: *const uci_list) -> *const uci_element {
// safety: uci_element.list has type uci_list, ptr points to uci_list
container_of!(ptr, uci_element, list)
}

/// casts an [`uci_element`] pointer to the containing [`uci_section`].
///
/// # Safety
/// The caller must ensure that `ptr` points to an element which is member of an [`uci_section`].
/// The `ptr` must not point to an element which is not contained in an uci_element.
pub unsafe fn uci_to_section(ptr: *const uci_element) -> *const uci_section {
// safety: uci_section.e has type uci_element, ptr points to uci_element
container_of!(ptr, uci_section, e)
}

/// mimics the C-macro `uci_foreach_element`
///
/// Note: the list head is not considered as a data node, and is skipped during iteration.
///
/// # Safety
/// The caller must ensure, that list points to a valid uci_list,
/// where each element is contained in a [`uci_element`] struct,
/// except for the list head.
///
/// The caller must not mutate the list during iteration (e.g. via `func`).
pub unsafe fn uci_foreach_element(list: *const uci_list, mut func: impl FnMut(*const uci_element)) {
if list.is_null() {
return;
}

let mut node = (*list).next;
while !node.is_null() && node.cast_const() != list {
let element = list_to_element(node.cast_const());
func(element);
node = (*node).next;
}
}

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

use crate::{
list_to_element, uci_element, uci_foreach_element, uci_list, uci_section, uci_to_section,
};

#[test]
fn list_to_element_succeeds() {
let elem = uci_element {
list: uci_list {
next: ptr::null_mut(),
prev: ptr::null_mut(),
},
type_: 0,
name: ptr::null_mut(),
};

assert_eq!(&raw const elem, unsafe {
list_to_element(&raw const elem.list)
});
}

#[test]
fn uci_to_section_succeeds() {
let section = uci_section {
e: uci_element {
list: uci_list {
next: ptr::null_mut(),
prev: ptr::null_mut(),
},
type_: 42,
name: ptr::null_mut(),
},
options: uci_list {
next: ptr::null_mut(),
prev: ptr::null_mut(),
},
package: ptr::null_mut(),
anonymous: false,
type_: ptr::null_mut(),
};

assert_eq!(&raw const section, unsafe {
uci_to_section(&raw const section.e)
});
}
#[test]
fn uci_foreach_element_succeeds() {
let mut head = uci_list {
next: ptr::null_mut(),
prev: ptr::null_mut(),
};
let mut e1 = uci_element {
list: uci_list {
prev: ptr::null_mut(),
next: ptr::null_mut(),
},
type_: 0,
name: ptr::null_mut(),
};
let mut e2 = e1;
let mut e3 = e2;

head.next = &raw mut e1.list;
head.prev = &raw mut e3.list;

e1.list.prev = &raw mut head;
e1.list.next = &raw mut e2.list;
e1.type_ = 1;

e2.list.prev = &raw mut e1.list;
e2.list.next = &raw mut e3.list;
e2.type_ = 2;

e3.list.prev = &raw mut e2.list;
e3.list.next = &raw mut head;
e3.type_ = 3;

let mut visited = vec![];
unsafe {
uci_foreach_element(&raw const head, |e: *const uci_element| {
visited.push((*e).type_);
})
};
assert_eq!(visited, [1, 2, 3]);
}
}
3 changes: 3 additions & 0 deletions rust-uci/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ targets = ["x86_64-unknown-linux-gnu"]
libuci-sys = { version = "^1.1.0", path = "../libuci-sys" }
log = "^0.4.14"
libc = "^0.2.91"

[dev-dependencies]
tempfile = "3"
138 changes: 135 additions & 3 deletions rust-uci/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ pub mod error;

use core::ptr;
use libuci_sys::{
uci_alloc_context, uci_commit, uci_context, uci_delete, uci_free_context, uci_get_errorstr,
uci_lookup_ptr, uci_option_type_UCI_TYPE_STRING, uci_ptr, uci_ptr_UCI_LOOKUP_COMPLETE,
uci_revert, uci_save, uci_set, uci_set_confdir, uci_set_savedir, uci_type_UCI_TYPE_OPTION,
uci_alloc_context, uci_commit, uci_context, uci_delete, uci_element, uci_foreach_element,
uci_free_context, uci_get_errorstr, uci_load, uci_lookup_ptr, uci_option_type_UCI_TYPE_STRING,
uci_package, uci_ptr, uci_ptr_UCI_LOOKUP_COMPLETE, uci_revert, uci_save, uci_set,
uci_set_confdir, uci_set_savedir, uci_to_section, uci_type_UCI_TYPE_OPTION,
uci_type_UCI_TYPE_SECTION, uci_unload,
};
use log::debug;
use std::str::FromStr;
use std::sync::Mutex;
use std::{
ffi::{CStr, CString},
Expand Down Expand Up @@ -458,6 +460,49 @@ impl Uci {
}
}

/// Queries all sections in a package from UCI.
/// If a package has been changed in the delta, the updated value will be returned.
///
/// Package values are like `network`, `wireless`, `dropbear`,...
///
/// If the package does not exist, an `Err` is returned.
pub fn get_sections(&mut self, package: impl AsRef<str>) -> Result<Vec<String>> {
let ptr = self.get_ptr(package.as_ref())?;
let pkg: *mut uci_package = ptr.p;
if pkg.is_null() {
return Err(Error::Message(format!(
"unable to load package {}",
package.as_ref()
)));
}

let mut sections = vec![];
unsafe {
// safety: - pkg is not null
// - The pkg->sections list is not mutated during iteration.
// - Each list entry in enclosed in an uci_element struct.
uci_foreach_element(&(*pkg).sections, |elem_ptr: *const uci_element| {
if elem_ptr.is_null() {
return;
}
// safety: the we iterate over pkg->sections, so all elements are contained in a section
let section = uci_to_section(elem_ptr);

// safety: elem_ptr is not null, so section is not null
let sec_name = (*section).e.name;
if sec_name.is_null() {
return;
}
// safety: - sec_name is not null
// - we have to trust that libuci only assigns valid C-strings, which are nul-terminated
if let Ok(sec_str) = CStr::from_ptr(sec_name).to_str() {
sections.push(sec_str.to_string());
}
})
};
Ok(sections)
}

/// Obtains the most recent error from UCI as a string
/// if no `last_error` is set, an `Err` is returned.
fn get_last_error(&mut self) -> Result<String> {
Expand All @@ -481,3 +526,90 @@ impl Uci {
}
}
}

#[cfg(test)]
mod tests {
use tempfile::{tempdir, TempDir};

use super::*;
fn setup_uci() -> Result<(Uci, TempDir)> {
let mut uci = Uci::new()?;
let tmp = tempdir().unwrap();
let config_dir = tmp.path().join("config");
let save_dir = tmp.path().join("save");

std::fs::create_dir_all(&config_dir).unwrap();
std::fs::create_dir_all(&save_dir).unwrap();

uci.set_config_dir(config_dir.as_os_str().to_str().unwrap())?;
uci.set_save_dir(save_dir.as_os_str().to_str().unwrap())?;
Ok((uci, tmp))
}

#[test]
fn list_wifi_sections_empty_list() {
let (mut uci, tmp) = setup_uci().unwrap();
let wireless_config_path = tmp.path().join("config/wireless");
std::fs::write(&wireless_config_path, "").unwrap();

let sections = uci.get_sections("wireless").unwrap();
assert_eq!(sections, Vec::<String>::new());
}

#[test]
fn list_wifi_sections_two_named_elements() {
let (mut uci, tmp) = setup_uci().unwrap();
let wireless_config_path = tmp.path().join("config/wireless");
std::fs::write(
&wireless_config_path,
"
config wifi-device 'pdev0'
option channel 'auto'

config wifi-iface 'wifi0'
option device 'pdev0'
",
)
.unwrap();

let sections = uci.get_sections("wireless").unwrap();
assert_eq!(sections, ["pdev0", "wifi0"]);
}

#[test]
fn list_wifi_sections_named_and_unnamed_elements() {
let (mut uci, tmp) = setup_uci().unwrap();
let wireless_config_path = tmp.path().join("config/wireless");
std::fs::write(
&wireless_config_path,
"
config wifi-device 'pdev0'
option channel 'auto'

config wifi-iface 'wifi0'
option device 'pdev0'

config wifi-iface
option device 'pdev0'
",
)
.unwrap();

let sections = uci.get_sections("wireless").unwrap();
assert_eq!(sections, ["pdev0", "wifi0", "cfg033579"]);
}

#[test]
fn list_sections_of_nonexistent_package() {
let (mut uci, tmp) = setup_uci().unwrap();
let wireless_config_path = tmp.path().join("config/wireless");
std::fs::write(&wireless_config_path, "").unwrap();

let sections = uci.get_sections("network");
// todo: refactor to assert_eq, once PartialEq is implemented on Error
println!("{sections:?}");
assert!(
matches!(sections, Err(Error::Message(str)) if str == "Could not parse uci key: network, 3, Entry not found")
);
}
}