diff --git a/libuci-sys/src/lib.rs b/libuci-sys/src/lib.rs index 0ab82cd..1a91eb8 100644 --- a/libuci-sys/src/lib.rs +++ b/libuci-sys/src/lib.rs @@ -31,12 +31,13 @@ pub use bindings::{ uci_commit, uci_context, uci_del_list, uci_delete, uci_delta, uci_element, uci_export, uci_flags, uci_free_context, uci_get_errorstr, uci_hash_options, uci_import, uci_list, uci_list_configs, uci_load, uci_lookup_next, uci_lookup_ptr, uci_option, uci_option_type, - uci_option_type_UCI_TYPE_STRING, uci_package, uci_parse_argument, uci_parse_context, - uci_parse_option, uci_parse_ptr, uci_parse_section, uci_perror, uci_ptr, - uci_ptr_UCI_LOOKUP_COMPLETE, uci_rename, uci_reorder_section, uci_revert, uci_save, - uci_section, uci_set, uci_set_backend, uci_set_confdir, uci_set_savedir, uci_type, - uci_type_UCI_TYPE_OPTION, uci_type_UCI_TYPE_SECTION, uci_unload, uci_validate_text, - UCI_ERR_NOTFOUND, UCI_OK, + uci_option_type_UCI_TYPE_LIST, uci_option_type_UCI_TYPE_STRING, uci_package, + uci_parse_argument, uci_parse_context, uci_parse_option, uci_parse_ptr, uci_parse_section, + uci_perror, uci_ptr, uci_ptr_UCI_LOOKUP_COMPLETE, uci_ptr_UCI_LOOKUP_EXTENDED, uci_rename, + uci_reorder_section, uci_revert, uci_save, uci_section, uci_set, uci_set_backend, + uci_set_confdir, uci_set_savedir, uci_type, uci_type_UCI_TYPE_OPTION, + uci_type_UCI_TYPE_PACKAGE, uci_type_UCI_TYPE_SECTION, uci_type_UCI_TYPE_UNSPEC, uci_unload, + uci_validate_text, UCI_ERR_NOTFOUND, UCI_OK, }; #[allow(clippy::ptr_offset_with_cast)] @@ -105,6 +106,16 @@ pub unsafe fn list_to_element(ptr: *const uci_list) -> *const uci_element { container_of!(ptr, uci_element, list) } +/// casts an [`uci_element`] pointer to the containing [`uci_package`]. +/// +/// # Safety +/// The caller must ensure that `ptr` points to an element which is member of an [`uci_package`]. +/// The `ptr` must not point to an element which is not contained in an uci_package. +pub unsafe fn uci_to_package(ptr: *const uci_element) -> *const uci_package { + // safety: uci_package.e has type uci_element, ptr points to uci_element + container_of!(ptr, uci_package, e) +} + /// casts an [`uci_element`] pointer to the containing [`uci_section`]. /// /// # Safety @@ -115,6 +126,16 @@ pub unsafe fn uci_to_section(ptr: *const uci_element) -> *const uci_section { container_of!(ptr, uci_section, e) } +/// casts an [`uci_element`] pointer to the containing [`uci_option`]. +/// +/// # Safety +/// The caller must ensure that `ptr` points to an element which is member of an [`uci_option`]. +/// The `ptr` must not point to an element which is not contained in an uci_element. +pub unsafe fn uci_to_option(ptr: *const uci_element) -> *const uci_option { + // safety: uci_option.e has type uci_element, ptr points to uci_element + container_of!(ptr, uci_option, e) +} + /// mimics the C-macro `uci_foreach_element` /// /// Note: the list head is not considered as a data node, and is skipped during iteration. diff --git a/rust-uci/src/config/mod.rs b/rust-uci/src/config/mod.rs new file mode 100644 index 0000000..25cd757 --- /dev/null +++ b/rust-uci/src/config/mod.rs @@ -0,0 +1,489 @@ +use std::{ + ffi::{c_char, CStr, CString}, + option::Option as StdOption, + sync::{Arc, Mutex}, +}; + +use libuci_sys::uci_list_configs; + +use crate::{ + error::{Error, Result}, + libuci_locked, Uci, UCI_ERR_NOTFOUND, UCI_OK, +}; + +mod option; +pub use option::{Option, OptionMut, Value}; + +mod package; +pub use package::Package; + +mod ptr; + +mod section; +pub use section::{Section, SectionIdent}; + +/// represents the root of the config tree +/// It's the parent structure to [Package]s +pub struct Config { + uci: Arc>, +} + +impl From for Config { + fn from(uci: Uci) -> Self { + Self { + uci: Arc::new(Mutex::new(uci)), + } + } +} + +struct PackageIter { + uci: Arc>, + original: *mut *mut c_char, + current: *mut *mut c_char, +} + +impl Iterator for PackageIter { + type Item = Package; + + fn next(&mut self) -> StdOption { + if self.current.is_null() { + return None; + } + let name_ptr = unsafe { *self.current }; + if name_ptr.is_null() { + return None; + } + self.current = unsafe { self.current.add(1) }; + let name = unsafe { CStr::from_ptr(name_ptr.cast()) }.to_owned(); + + Some(Package::new(Arc::clone(&self.uci), Arc::new(name))) + } +} + +impl Drop for PackageIter { + fn drop(&mut self) { + unsafe { libc::free(self.original.cast::()) } + } +} + +impl Config { + pub fn new() -> Result { + Ok(Uci::new()?.into()) + } + + /// return a single [Package] by its name + /// also works if the package is not defined yet + pub fn package<'a>(&self, name: impl AsRef) -> Result> { + let cname = CString::new(name.as_ref())?; + let pkg = Package::new(Arc::clone(&self.uci), Arc::new(cname)); + let mut uci = self.uci.lock().unwrap(); + Ok(pkg.ptr_opt(&mut uci)?.map(|_| pkg)) + } + + /// list all [Package]s in the config + pub fn packages<'a>(&self) -> Result> { + let mut uci = self.uci.lock().unwrap(); + let mut list = std::ptr::null_mut(); + let result = libuci_locked!(unsafe { uci_list_configs(uci.ctx, &mut list) }); + let ptr = match handle_error(&mut uci, result)? { + Some(_) => list, + None => std::ptr::null_mut(), + }; + Ok(PackageIter { + uci: Arc::clone(&self.uci), + original: ptr, + current: ptr, + }) + } + + /// save all packages to the temporary delta + pub fn save_all(&mut self) -> Result<()> { + for mut pkg in self.packages()? { + pkg.save()?; + } + Ok(()) + } + + /// commit all packages from the delta to the config on disk + pub fn commit_all(&mut self) -> Result<()> { + for mut pkg in self.packages()? { + pkg.commit()?; + } + Ok(()) + } +} + +fn handle_error(uci: &mut Uci, result: i32) -> Result> { + match result { + UCI_OK => Ok(Some(())), + UCI_ERR_NOTFOUND => { + return Ok(None); + } + _ => { + return Err(Error::Message( + uci.get_last_error() + .unwrap_or_else(|_| String::from("Unknown")), + )); + } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{tempdir, TempDir}; + + use super::{option::Value, section::SectionIdent, *}; + + 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 get_option() { + let (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 cfg: Config = uci.into(); + let pkg = cfg.package("wireless").unwrap().unwrap(); + let sect = pkg + .section("wifi-device", SectionIdent::Named("pdev0")) + .unwrap(); + let opt = sect.option("channel").unwrap(); + let val = opt.get().unwrap(); + assert_eq!(Some(option::Value::String("auto".into())), val); + } + + #[test] + fn set_option_existing() { + let (uci, tmp) = setup_uci().unwrap(); + std::fs::write( + &tmp.path().join("config/wireless"), + " + config wifi-device 'pdev0' + option channel 'auto' + ", + ) + .unwrap(); + + let cfg = Config::from(uci); + let mut opt = cfg + .package("wireless") + .unwrap() + .unwrap() + .section("wifi-device", "pdev0") + .unwrap() + .option_mut("channel") + .unwrap(); + opt.set("44").unwrap(); + assert_eq!(Value::String("44".into()), opt.get().unwrap().unwrap()); + } + + #[test] + fn set_option_new() { + let (uci, tmp) = setup_uci().unwrap(); + std::fs::write( + &tmp.path().join("config/wireless"), + " + config wifi-device 'pdev0' + option channel 'auto' + ", + ) + .unwrap(); + let save_dir = uci.get_save_dir().unwrap().to_owned(); + let config_dir = uci.get_config_dir().unwrap().to_owned(); + + let mut cfg = Config::from(uci); + + { + let mut opt = cfg + .package("wireless") + .unwrap() + .unwrap() + .section("wifi-device", "pdev0") + .unwrap() + .option_mut("disabled") + .unwrap(); + opt.set("1").unwrap(); + assert_eq!(Value::String("1".into()), opt.get().unwrap().unwrap()); + } + + { + // re-get, unsaved + let v = cfg + .package("wireless") + .unwrap() + .unwrap() + .section("wifi-device", "pdev0") + .unwrap() + .option("disabled") + .unwrap() + .get() + .unwrap() + .unwrap(); + assert_eq!(Value::String("1".into()), v); + } + + cfg.save_all().unwrap(); + + { + // recreate uci instance + let mut uci = Uci::new().unwrap(); + uci.set_save_dir(&save_dir).unwrap(); + uci.set_config_dir(&config_dir).unwrap(); + let cfg = Config::from(uci); + + // saved + let v = cfg + .package("wireless") + .unwrap() + .unwrap() + .section("wifi-device", "pdev0") + .unwrap() + .option("disabled") + .unwrap() + .get() + .unwrap() + .unwrap(); + assert_eq!(Value::String("1".into()), v); + } + } + + #[test] + fn set_option_new_section() { + let (uci, tmp) = setup_uci().unwrap(); + std::fs::write( + &tmp.path().join("config/wireless"), + " + config wifi-device 'pdev0' + option channel 'auto' + ", + ) + .unwrap(); + + let cfg = Config::from(uci); + + { + let mut opt = cfg + .package("wireless") + .unwrap() + .unwrap() + .section("wifi-device", "pdev1") + .unwrap() + .option_mut("channel") + .unwrap(); + opt.set("auto").unwrap(); + assert_eq!(Value::String("auto".into()), opt.get().unwrap().unwrap()); + } + + { + let v = cfg + .package("wireless") + .unwrap() + .unwrap() + .section("wifi-device", "pdev1") + .unwrap() + .option("channel") + .unwrap() + .get() + .unwrap() + .unwrap(); + assert_eq!(Value::String("auto".into()), v); + } + } + + #[test] + fn create_section_anonymous() { + let (uci, tmp) = setup_uci().unwrap(); + std::fs::write( + &tmp.path().join("config/wireless"), + " + config wifi-device 'pdev0' + option channel 'auto' + ", + ) + .unwrap(); + + let cfg = Config::from(uci); + let mut section = cfg + .package("wireless") + .unwrap() + .unwrap() + .section("wifi-device", ()) + .unwrap(); + // will implictly create the option + section.option_mut("channel").unwrap().set("auto").unwrap(); + assert_eq!( + Value::String("auto".into()), + section.option("channel").unwrap().get().unwrap().unwrap() + ); + assert!(section.name().unwrap().len() > 0); + + let pkg = cfg.package("wireless").unwrap().unwrap(); + assert_eq!(2, pkg.sections().unwrap().count()); + } + + #[test] + fn list_packages() { + let (uci, tmp) = setup_uci().unwrap(); + std::fs::write( + &tmp.path().join("config/wireless"), + " + config wifi-device 'pdev0' + option channel 'auto' + ", + ) + .unwrap(); + std::fs::write( + &tmp.path().join("config/network"), + " + config device 'eth0' + option mtu '1280' + ", + ) + .unwrap(); + + let cfg: Config = uci.into(); + let pkgs: Vec<_> = cfg.packages().unwrap().collect(); + assert_eq!(2, pkgs.len()); + for pkg in pkgs { + match pkg.name().unwrap() { + "wireless" => (), + "network" => (), + n => panic!("Unexpected name: {}", n), + } + } + } + + #[test] + fn list_sections() { + let (uci, tmp) = setup_uci().unwrap(); + std::fs::write( + &tmp.path().join("config/wireless"), + " + config wifi-device 'pdev0' + option channel 'auto' + + config wifi-device 'pdev1' + list channel '44' + list channel '48' + + config wifi-device + option channel '56' + ", + ) + .unwrap(); + + let cfg = Config::from(uci); + let pkg = cfg.package("wireless").unwrap().unwrap(); + let sections: Vec<_> = pkg.sections().unwrap().collect(); + assert_eq!(3, sections.len()); + + for section in §ions { + assert_eq!("wifi-device", section.type_()); + let channel = section.option("channel").unwrap().get().unwrap().unwrap(); + match section.name().unwrap().as_str() { + "pdev0" => assert_eq!(Value::String("auto".into()), channel), + "pdev1" => assert_eq!(Value::List(vec!["44".into(), "48".into()]), channel), + _ => assert_eq!(Value::String("56".into()), channel), + } + } + } + + #[test] + fn list_sections_by_type() { + let (uci, tmp) = setup_uci().unwrap(); + std::fs::write( + &tmp.path().join("config/wireless"), + " + config wifi-device 'pdev0' + option channel 'auto' + + config wifi-iface 'wlan0' + option device 'pdev0' + + config wifi-iface + option device 'pdev0' + ", + ) + .unwrap(); + + let cfg = Config::from(uci); + let pkg = cfg.package("wireless").unwrap().unwrap(); + + let all_sections: Vec<_> = pkg.sections().unwrap().collect(); + assert_eq!(3, all_sections.len()); + + let iface_sections: Vec<_> = pkg.sections_by_type("wifi-iface").unwrap().collect(); + assert_eq!(2, iface_sections.len()); + + for sect in iface_sections { + assert_eq!("wifi-iface", sect.type_()); + assert_eq!( + Value::String("pdev0".into()), + sect.option("device").unwrap().get().unwrap().unwrap() + ) + } + } + + #[test] + fn list_options() { + let (uci, tmp) = setup_uci().unwrap(); + std::fs::write( + &tmp.path().join("config/wireless"), + " + config wifi-device 'pdev0' + list channel '44' + list channel '48' + option disabled '0' + option txpower '56' + option country 'DE' + option log_level '4' + ", + ) + .unwrap(); + + let cfg = Config::from(uci); + let section = cfg + .package("wireless") + .unwrap() + .unwrap() + .section("wifi-device", "pdev0") + .unwrap(); + for opt in section.options().unwrap() { + let v = opt.get().unwrap().unwrap(); + let expected = match opt.name() { + "channel" => { + assert_eq!(Value::List(vec!["44".into(), "48".into()]), v); + continue; + } + "disabled" => "0", + "txpower" => "56", + "country" => "DE", + "log_level" => "4", + _ => panic!("unexpected option: {}", opt.name()), + }; + assert_eq!(Value::String(expected.into()), v); + } + } +} diff --git a/rust-uci/src/config/option.rs b/rust-uci/src/config/option.rs new file mode 100644 index 0000000..47e2194 --- /dev/null +++ b/rust-uci/src/config/option.rs @@ -0,0 +1,250 @@ +use core::slice; +use std::{ + borrow::Cow, + ffi::{CStr, CString}, + ops::DerefMut, + option::Option as StdOption, + sync::{Arc, Mutex}, +}; + +use libuci_sys::{ + uci_add_list, uci_delete, uci_foreach_element, uci_option_type_UCI_TYPE_LIST, + uci_option_type_UCI_TYPE_STRING, uci_set, uci_type_UCI_TYPE_OPTION, +}; + +use crate::{config::handle_error, error::Error, libuci_locked, Result, Uci}; + +use super::{ + ptr::UciPtr, + section::{Section, SectionIdent}, +}; + +pub type OptionMut = Option; + +/// represents an option within a [Section] +pub struct Option { + uci: Arc>, + package: Arc, + section: (Arc, Arc>), + name: Arc, +} + +impl Option { + pub(crate) fn new( + uci: Arc>, + package: Arc, + section: (Arc, Arc>), + name: Arc, + ) -> Option { + Option { + uci, + package, + section, + name, + } + } + + fn ptr<'a>(&'_ self, uci: &'a mut Uci) -> Result>> { + let section = match self.section().ptr(uci)? { + Some(s) => s, + None => return Ok(None), + }; + + let mut ptr = UciPtr::new(); + ptr.target = uci_type_UCI_TYPE_OPTION; + ptr.p = section.p; + ptr.s = section.s; + ptr.option = self.name.as_ptr(); + ptr.lookup(uci) + } + + fn ptr_ensure<'a>(&'_ mut self, uci: &'a mut Uci) -> Result> { + let mut section = self.section(); + let section_ptr = section.ensure(Some(uci))?; + + // update ident to match newly created item + self.section.1 = Arc::clone(§ion.ident); + + let mut ptr = UciPtr::new(); + ptr.target = uci_type_UCI_TYPE_OPTION; + ptr.p = section_ptr.p; + ptr.s = section_ptr.s; + ptr.option = self.name.as_ptr(); + Ok(ptr) + } + + /// name of the option + pub fn name(&self) -> &str { + self.name.to_str().unwrap() + } + + pub fn section(&self) -> Section { + Section::new( + Arc::clone(&self.uci), + Arc::clone(&self.package), + Arc::clone(&self.section.0), + Arc::clone(&self.section.1), + ) + } + + /// returns the current value of the option, None if not set + pub fn get<'a>(&'a self) -> Result> { + let mut uci = self.uci.lock().unwrap(); + let ptr = match self.ptr(&mut uci)? { + Some(ptr) => ptr, + None => return Ok(None), + }; + + let opt = ptr.o; + + #[allow(non_upper_case_globals)] + match unsafe { *opt }.type_ { + uci_option_type_UCI_TYPE_STRING => { + let raw = unsafe { CStr::from_ptr((*opt).v.string) }; + Ok(Value::String(raw.to_str()?.into())) + } + uci_option_type_UCI_TYPE_LIST => { + let mut result = Vec::new(); + unsafe { + uci_foreach_element(&(*opt).v.list, |elem| { + let raw = CStr::from_ptr((*elem).name); + result.push(raw); + }) + }; + Ok(Value::List( + result + .into_iter() + .map(|cstr| cstr.to_str().map_err(Into::into).map(Into::into)) + .collect::>>()?, + )) + } + t => return Err(Error::Message(format!("Unexpected option type: {t}"))), + } + .map(Some) + } +} + +impl OptionMut { + /// sets the value of the option, overriding the previous value + /// will create the [Package] or [Section] along the way if they do + /// not exist + pub fn set(&mut self, value: impl Into) -> Result<()> { + let uci = Arc::clone(&self.uci); + let mut uci = uci.lock().unwrap(); + let ptr = self.ptr_ensure(&mut uci)?; + let mut ptr: UciPtr<'static> = unsafe { std::mem::transmute(ptr) }; + match value.into() { + Value::String(s) => { + let value = CString::new(s)?; + ptr.value = value.as_ptr(); + + let result = libuci_locked!(unsafe { uci_set(uci.ctx, &raw mut *ptr.deref_mut()) }); + handle_error(&mut uci, result)?; + } + Value::List(items) => { + libuci_locked!({ + let result = unsafe { uci_delete(uci.ctx, &raw mut *ptr.deref_mut()) }; + handle_error(&mut uci, result)?; + for item in items { + let val = CString::new(item)?; + ptr.value = val.as_ptr(); + let result = unsafe { uci_add_list(uci.ctx, &raw mut *ptr.deref_mut()) }; + handle_error(&mut uci, result)?; + } + }); + } + }; + Ok(()) + } + + /// adds a value to the existing value + /// behaves like `uci add_list` which will: + /// - create the option if it doesn't exist (not as a list) + /// - turn a single-value option into a list + /// + /// returns the resulting value + pub fn add_list(&mut self, value: impl AsRef) -> Result<()> { + let value = CString::new(value.as_ref())?; + + let uci = Arc::clone(&self.uci); + let mut uci = uci.lock().unwrap(); + let mut ptr = self.ptr_ensure(&mut uci)?; + ptr.value = value.as_ptr(); + + let mut uci = self.uci.lock().unwrap(); + let result = libuci_locked!(unsafe { uci_add_list(uci.ctx, &raw mut *ptr.deref_mut()) }); + handle_error(&mut uci, result)?; + + Ok(()) + } +} + +/// represents the value of an [Option] +#[derive(Debug)] +pub enum Value { + String(String), + List(Vec), +} + +impl Value { + pub fn list<'a, I: Into>>(val: impl IntoIterator) -> Self { + let v = val.into_iter().map(|s| s.into().to_string()).collect(); + Self::List(v) + } + + pub fn to_str(&self) -> StdOption<&str> { + match self { + Value::String(n) => Some(n), + _ => None, + } + } +} + +pub enum ValueIter<'a> { + String(StdOption<&'a String>), + List(slice::Iter<'a, String>), +} + +impl<'a> Iterator for ValueIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> StdOption { + match self { + ValueIter::String(opt) => opt.take(), + ValueIter::List(iter) => iter.next(), + } + .map(String::as_str) + } +} + +impl<'a> IntoIterator for &'a Value { + type Item = &'a str; + + type IntoIter = ValueIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + match self { + Value::String(val) => ValueIter::String(Some(val)), + Value::List(items) => ValueIter::List(items.iter()), + } + } +} + +impl From for Value +where + T: ToString, +{ + fn from(value: T) -> Self { + Self::String(value.to_string()) + } +} + +impl<'a> PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::String(l0), Self::String(r0)) => l0 == r0, + (Self::List(l0), Self::List(r0)) => l0 == r0, + _ => false, + } + } +} diff --git a/rust-uci/src/config/package.rs b/rust-uci/src/config/package.rs new file mode 100644 index 0000000..2063d64 --- /dev/null +++ b/rust-uci/src/config/package.rs @@ -0,0 +1,135 @@ +use std::{ + ffi::{CStr, CString}, + option::Option as StdOption, + ptr, + sync::{Arc, Mutex}, +}; + +use libuci_sys::{uci_commit, uci_element, uci_save, uci_to_section, uci_type_UCI_TYPE_PACKAGE}; + +use crate::{ + config::{handle_error, section::SectionIdent}, + error::Error, + libuci_locked, Result, Uci, +}; + +use super::{ + ptr::{UciListIter, UciPtr}, + section::Section, +}; + +/// represents a single package in the config tree +/// parent to different [Section]s +pub struct Package { + uci: Arc>, + name: Arc, +} + +impl Package { + pub(crate) fn new(uci: Arc>, name: Arc) -> Package { + Package { uci, name } + } + + pub fn name(&self) -> Result<&str> { + Ok(self.name.to_str()?) + } + + pub(crate) fn ptr_opt<'a>(&'_ self, uci: &'a mut Uci) -> Result>> { + let mut ptr = UciPtr::new(); + ptr.target = uci_type_UCI_TYPE_PACKAGE; + ptr.package = self.name.as_c_str().as_ptr(); + ptr.lookup(uci) + } + + pub(crate) fn ptr<'a>(&'_ self, uci: &'a mut Uci) -> Result> { + match self.ptr_opt(uci)? { + Some(ptr) => Ok(ptr), + None => Err(Error::EntryNotFound { + entry_identifier: self.name()?.to_owned(), + }), + } + } + + fn sections_impl bool>( + &self, + filter: F, + ) -> Result> { + let mut uci = self.uci.lock().unwrap(); + let ptr = match self.ptr_opt(&mut uci)? { + Some(ptr) => unsafe { &(*ptr.p).sections }, + None => ptr::null(), + }; + drop(uci); + let uci = Arc::clone(&self.uci); + let package = Arc::clone(&self.name); + Ok(UciListIter::new(ptr).filter(filter).map(move |elem| { + let sect = unsafe { uci_to_section(elem) }; + let type_ = unsafe { CStr::from_ptr((*sect).type_) }.to_owned(); + let name = unsafe { CStr::from_ptr((*elem).name) }.to_owned(); + Section::new( + Arc::clone(&uci), + Arc::clone(&package), + Arc::new(type_), + Arc::new(SectionIdent::Named(name)), + ) + })) + } + + pub fn sections(&self) -> Result> { + self.sections_impl(|_| true) + } + + pub fn sections_by_type( + &self, + type_: impl AsRef, + ) -> Result> { + let type_ = CString::new(type_.as_ref())?; + self.sections_impl(move |e| { + let elem_type = unsafe { CStr::from_ptr((*uci_to_section(*e)).type_) }; + elem_type == type_.as_c_str() + }) + } + + /// return a single [Section] by its name + /// also works if the section is not defined yet + pub fn section>( + &self, + type_: impl AsRef, + ident: impl Into>, + ) -> Result
{ + let type_ = CString::new(type_.as_ref())?; + + use SectionIdent::*; + let ident = match ident.into() { + Anonymous => Anonymous, + Indexed(i) => Indexed(i), + Named(n) => Named(CString::new(n.as_ref())?), + }; + + Ok(Section::new( + Arc::clone(&self.uci), + Arc::clone(&self.name), + Arc::new(type_), + Arc::new(ident), + )) + } + + /// save package delta to disk + pub fn save(&mut self) -> Result<()> { + let mut uci = self.uci.lock().unwrap(); + let pkg = self.ptr(&mut uci)?.p; + let result = libuci_locked!(unsafe { uci_save(uci.ctx, pkg) }); + handle_error(&mut uci, result)?; + Ok(()) + } + + /// commit package delta into real config on disk + pub fn commit(&mut self) -> Result<()> { + let mut uci = self.uci.lock().unwrap(); + let mut pkg = self.ptr(&mut uci)?.p; + // the uci cli seems to set `override=false` too, not sure what it means + let result = libuci_locked!(unsafe { uci_commit(uci.ctx, &raw mut pkg, false) }); + handle_error(&mut uci, result)?; + Ok(()) + } +} diff --git a/rust-uci/src/config/ptr.rs b/rust-uci/src/config/ptr.rs new file mode 100644 index 0000000..8767fed --- /dev/null +++ b/rust-uci/src/config/ptr.rs @@ -0,0 +1,122 @@ +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, + option::Option as StdOption, + ptr, +}; + +use libuci_sys::{ + list_to_element, uci_element, uci_list, uci_lookup_ptr, uci_ptr, uci_ptr_UCI_LOOKUP_COMPLETE, + uci_type_UCI_TYPE_UNSPEC, +}; + +use crate::{config::handle_error, libuci_locked, Result, Uci}; + +/// mimicks the `uci_foreach_element_safe` macro in libuci +/// supposedly deletion safe +pub(super) struct UciListIter<'a> { + list: *const uci_list, // head of the list, doesn't change + ptr: *const uci_element, // next element + tmp: *const uci_element, // element after ptr + _lt: &'a PhantomData<()>, +} + +impl<'a> UciListIter<'a> { + pub fn new(list: *const uci_list) -> Self { + let ptr = if list.is_null() { + ptr::null() + } else { + unsafe { list_to_element((*list).next) } + }; + let tmp = if ptr.is_null() { + ptr::null() + } else { + unsafe { list_to_element((*ptr).list.next) } + }; + Self { + list, + ptr, + tmp, + _lt: &PhantomData, + } + } +} + +impl<'a> Iterator for UciListIter<'a> { + type Item = *const uci_element; + + fn next(&mut self) -> StdOption { + if self.ptr.is_null() { + return None; + } + if unsafe { &raw const (*self.ptr).list } == self.list { + return None; + } + + let elem = self.ptr; + + self.ptr = self.tmp; + self.tmp = unsafe { list_to_element((*self.ptr).list.next) }; + + Some(elem) + } +} + +pub(crate) struct UciPtr<'a> { + ptr: uci_ptr, + _lt: &'a PhantomData<()>, +} + +impl Deref for UciPtr<'_> { + type Target = uci_ptr; + + fn deref(&self) -> &Self::Target { + &self.ptr + } +} + +impl DerefMut for UciPtr<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ptr + } +} + +impl<'a> UciPtr<'a> { + pub fn new() -> UciPtr<'static> { + let ptr = uci_ptr { + target: uci_type_UCI_TYPE_UNSPEC, + flags: 0, + p: ptr::null_mut(), + s: ptr::null_mut(), + o: ptr::null_mut(), + last: ptr::null_mut(), + package: ptr::null(), + section: ptr::null(), + option: ptr::null(), + value: ptr::null(), + }; + UciPtr { + ptr, + _lt: &PhantomData, + } + } + + pub fn lookup<'b>(&'_ mut self, uci: &'b mut Uci) -> Result>> { + let mut ptr = self.ptr.clone(); + let result = + libuci_locked!(unsafe { uci_lookup_ptr(uci.ctx, &mut ptr, ptr::null_mut(), true) }); + let ptr = match handle_error(uci, result)? { + Some(_) => { + if ptr.flags & uci_ptr_UCI_LOOKUP_COMPLETE == 0 { + return Ok(None); + } + ptr + } + None => return Ok(None), + }; + Ok(Some(UciPtr { + ptr, + _lt: &PhantomData, + })) + } +} diff --git a/rust-uci/src/config/section.rs b/rust-uci/src/config/section.rs new file mode 100644 index 0000000..3eb1c33 --- /dev/null +++ b/rust-uci/src/config/section.rs @@ -0,0 +1,239 @@ +use std::{ + ffi::{CStr, CString}, + iter, + ops::DerefMut, + option::Option as StdOption, + ptr, + sync::{Arc, Mutex}, +}; + +use libuci_sys::{ + uci_add_section, uci_ptr_UCI_LOOKUP_EXTENDED, uci_set, uci_type_UCI_TYPE_SECTION, +}; + +use crate::{ + config::{handle_error, option::OptionMut}, + libuci_locked, Result, Uci, +}; + +use super::{ + option::Option, + package::Package, + ptr::{UciListIter, UciPtr}, +}; + +pub enum SectionIdent { + Anonymous, + Indexed(i32), + Named(T), +} + +impl SectionIdent +where + T: AsRef, +{ + pub(crate) fn inner_ident(&self, type_: impl AsRef) -> StdOption { + match self { + SectionIdent::Anonymous => None, + SectionIdent::Indexed(i) => { + let type_ = type_.as_ref().to_bytes(); + let indexer = format!("[{i}]").into_bytes(); + let ident: Vec<_> = iter::once('@' as u8) + .chain(type_.into_iter().copied()) + .chain(indexer) + .collect(); + Some(CString::new(ident).unwrap()) + } + SectionIdent::Named(name) => Some(name.as_ref().to_owned()), + } + } +} + +impl<'a> From<&'a str> for SectionIdent<&'a str> { + fn from(value: &'a str) -> Self { + Self::Named(value) + } +} + +impl From for SectionIdent { + fn from(value: String) -> Self { + Self::Named(value) + } +} + +impl From for SectionIdent { + fn from(value: i32) -> Self { + Self::Indexed(value) + } +} + +impl From<()> for SectionIdent { + fn from(_value: ()) -> Self { + Self::Anonymous + } +} + +/// represents a single section +/// parent to different [Option]s +pub struct Section { + uci: Arc>, + package: Arc, + pub(crate) type_: Arc, + pub(crate) ident: Arc>, +} + +impl Section { + pub(crate) fn new( + uci: Arc>, + package: Arc, + type_: Arc, + ident: Arc>, + ) -> Self { + Self { + uci, + package, + type_, + ident, + } + } + + pub(crate) fn ptr<'a>(&'_ self, uci: &'a mut Uci) -> Result>> { + let mut ptr = UciPtr::new(); + + let _ident_raw = match &*self.ident { + SectionIdent::Anonymous => return Ok(None), + i @ SectionIdent::Indexed(_) => { + let ident = i.inner_ident(self.type_.as_ref()).unwrap(); + ptr.section = ident.as_ptr(); + ptr.flags |= uci_ptr_UCI_LOOKUP_EXTENDED; + Some(ident) // keep this alive + } + SectionIdent::Named(s) => { + ptr.section = s.as_ptr(); + None + } + }; + + ptr.target = uci_type_UCI_TYPE_SECTION; + ptr.package = self.package.as_c_str().as_ptr(); + ptr.lookup(uci) + } + + pub(crate) fn ensure<'a>(&'_ mut self, uci: StdOption<&'a mut Uci>) -> Result> { + let mut guard = None; + let uci = match uci { + Some(uci) => uci, + None => { + guard.replace(self.uci.lock().unwrap()); + guard.as_deref_mut().unwrap() + } + }; + let pkg_ptr = self.package().ptr(uci)?; + + let mut ptr = UciPtr::new(); + ptr.target = uci_type_UCI_TYPE_SECTION; + ptr.p = pkg_ptr.p; + + let result = match self.ident.as_ref() { + SectionIdent::Anonymous => { + let mut section_ptr = ptr::null_mut(); + let result = libuci_locked!(unsafe { + uci_add_section(uci.ctx, ptr.p, self.type_.as_ptr(), &mut section_ptr) + }); + ptr.s = section_ptr; + + // persist created name in the ident + let name = unsafe { CStr::from_ptr((*section_ptr).e.name) }.to_owned(); + self.ident = Arc::new(SectionIdent::Named(name)); + + result + } + i @ SectionIdent::Indexed(_) => { + let ident = i.inner_ident(self.type_.as_ref()).unwrap(); + ptr.flags |= uci_ptr_UCI_LOOKUP_EXTENDED; + ptr.section = ident.as_c_str().as_ptr(); + ptr.value = self.type_.as_ptr(); + let result = libuci_locked!(unsafe { uci_set(uci.ctx, ptr.deref_mut()) }); + ptr.section = ptr::null(); // CString is dropped after this context + result + } + SectionIdent::Named(name) => { + ptr.section = name.as_ptr(); + ptr.value = self.type_.as_ptr(); + libuci_locked!(unsafe { uci_set(uci.ctx, ptr.deref_mut()) }) + } + }; + handle_error(uci, result)?; + + Ok(ptr) + } + + pub fn create(&mut self) -> Result<()> { + self.ensure(None)?; + Ok(()) + } + + /// returns the name of the section item, None if it's anonymous + pub fn name(&self) -> StdOption { + let ident = self.ident.as_ref().inner_ident(self.type_.as_ref()); + ident.map(|cstr| cstr.into_string().unwrap()) + } + + /// returns the type of the section + pub fn type_(&self) -> &str { + self.type_.to_str().unwrap() + } + + /// lists all options in this section + pub fn options(&self) -> Result> { + let mut uci = self.uci.lock().unwrap(); + let section = self.ptr(&mut uci)?.map(|p| unsafe { *p.s }); + let option_list = section + .map(|l| &raw const l.options) + .unwrap_or_else(ptr::null); + + let uci = Arc::clone(&self.uci); + let package = Arc::clone(&self.package); + let section_type = Arc::clone(&self.type_); + let section_ident = Arc::clone(&self.ident); + Ok(UciListIter::new(option_list).map(move |elem| { + let name = unsafe { CStr::from_ptr((*elem).name) }.to_owned(); + Option::new( + Arc::clone(&uci), + Arc::clone(&package), + (Arc::clone(§ion_type), Arc::clone(§ion_ident)), + Arc::new(name), + ) + })) + } + + /// returns a specific [Option] by name + /// also works if the option is not defined yet + pub fn option(&self, name: impl AsRef) -> Result