Skip to content
207 changes: 124 additions & 83 deletions rust-uci/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ pub mod error;

use core::ptr;
use libuci_sys::{
uci_alloc_context, uci_commit, uci_context, uci_delete, uci_element, uci_foreach_element,
uci_free_context, uci_get_errorstr, 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_add_list, uci_alloc_context, uci_commit, uci_context, uci_delete, uci_element,
uci_foreach_element, uci_free_context, uci_get_errorstr, 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;
Expand All @@ -66,6 +66,8 @@ use std::sync::Mutex;
use std::{
ffi::{CStr, CString},
ops::{Deref, DerefMut},
os::unix::ffi::OsStrExt,
path::Path,
};

use crate::error::{Error, Result};
Expand Down Expand Up @@ -141,27 +143,36 @@ unsafe impl Sync for Uci {}
/// - [`uci_context`] can be freed safely from another thread.
unsafe impl Send for Uci {}

/// Contains the native `uci_ptr` and it's raw `CString` key
/// this is done so the raw `CString` stays alive until the `uci_ptr` is dropped
struct UciPtr(uci_ptr, *mut std::os::raw::c_char);
/// Contains the native `uci_ptr` and it's associated key.
struct UciPtr {
ptr: uci_ptr,
_key: CString,
}

impl UciPtr {
/// Assert that self.ptr.value is set (non-null), returning self if the value is set.
fn assert_value_set(self) -> Result<Self> {
if self.ptr.value.is_null() {
return Err(Error::Message(format!(
"uci_ptr.value is null: {}",
self._key.to_string_lossy(),
)));
}
Ok(self)
}
}

impl Deref for UciPtr {
type Target = uci_ptr;

fn deref(&self) -> &Self::Target {
&self.0
&self.ptr
}
}

impl DerefMut for UciPtr {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl Drop for UciPtr {
fn drop(&mut self) {
drop(unsafe { CString::from_raw(self.1) });
&mut self.ptr
}
}

Expand All @@ -179,7 +190,7 @@ impl Drop for UciPtr {
/// and returns an `Err` otherwise.
unsafe fn char_ptr_to_str<'a>(ptr: *mut c_char) -> Result<&'a str> {
if ptr.is_null() {
return Err(Error::Message("config dir was nullptr".into()));
return Err(Error::Message("char* was nullptr".into()));
}
// Safety: the ptr is not null.
// The safety assumption is that ptr points to
Expand All @@ -203,24 +214,24 @@ impl Uci {
}

/// Sets the config directory of UCI, this is `/etc/config` by default.
pub fn set_config_dir(&mut self, config_dir: &str) -> Result<()> {
pub fn set_config_dir(&mut self, config_dir: impl AsRef<Path>) -> Result<()> {
libuci_locked!(self, {
let result = unsafe {
let raw = CString::new(config_dir)?;
uci_set_confdir(
self.ctx,
raw.as_bytes_with_nul()
.as_ptr()
.cast::<std::os::raw::c_char>(),
)
let result = {
let config_dir = config_dir.as_ref().as_os_str();
let config_dir = CString::new(config_dir.as_bytes())?;
// Safety:
// * self.ctx points to a valid UCI context.
// * config_dir is a valid, null-terminated C-string.
// * config_dir is a valid directory path.
unsafe { uci_set_confdir(self.ctx, config_dir.as_ptr()) }
};
if result == UCI_OK {
debug!("Set config dir to: {}", config_dir);
debug!("Set config dir to: {}", config_dir.as_ref().display());
Ok(())
} else {
Err(Error::Message(format!(
"Cannot set config dir: {}, {}",
config_dir,
config_dir.as_ref().display(),
self.get_last_error()
.unwrap_or_else(|_| String::from("Unknown"))
)))
Expand All @@ -246,24 +257,24 @@ impl Uci {
}

/// Sets the save directory of UCI, this is `/tmp/.uci` by default.
pub fn set_save_dir(&mut self, save_dir: &str) -> Result<()> {
let raw = CString::new(save_dir)?;
pub fn set_save_dir(&mut self, save_dir: impl AsRef<Path>) -> Result<()> {
libuci_locked!(self, {
let result = unsafe {
uci_set_savedir(
self.ctx,
raw.as_bytes_with_nul()
.as_ptr()
.cast::<std::os::raw::c_char>(),
)
let result = {
let save_dir = save_dir.as_ref().as_os_str();
let save_dir = CString::new(save_dir.as_bytes())?;
// Safety:
// * self.ctx points to a valid UCI context.
// * save_dir is a valid, null-terminated C-string.
// * save_dir is a valid directory path.
unsafe { uci_set_savedir(self.ctx, save_dir.as_ptr()) }
};
if result == UCI_OK {
debug!("Set save dir to: {}", save_dir);
debug!("Set save dir to: {}", save_dir.as_ref().display());
Ok(())
} else {
Err(Error::Message(format!(
"Cannot set save dir: {}, {}",
save_dir,
save_dir.as_ref().display(),
self.get_last_error()
.unwrap_or_else(|_| String::from("Unknown"))
)))
Expand Down Expand Up @@ -296,9 +307,9 @@ impl Uci {
/// If the deletion failed an `Err` is returned.
/// If the entry does not exist before deletion, an `Err` with [`Error::EntryNotFound`] is returned.
pub fn delete(&mut self, identifier: &str) -> Result<()> {
let mut ptr = self.get_ptr(identifier)?;
libuci_locked!(self, {
let result = unsafe { uci_delete(self.ctx, &mut ptr.0) };
let mut ptr = self.get_ptr(identifier)?;
let result = unsafe { uci_delete(self.ctx, ptr.deref_mut()) };
if result != UCI_OK {
return Err(Error::Message(format!(
"Could not delete uci key: {}, {}, {}",
Expand All @@ -308,21 +319,9 @@ impl Uci {
.unwrap_or_else(|_| String::from("Unknown"))
)));
}
let result = unsafe { uci_save(self.ctx, ptr.p) };
match result {
UCI_OK => Ok(()),
UCI_ERR_NOTFOUND => Err(Error::EntryNotFound {
entry_identifier: identifier.to_string(),
}),
_ => Err(Error::Message(format!(
"Could not save uci key: {}, {}, {}",
identifier,
result,
self.get_last_error()
.unwrap_or_else(|_| String::from("Unknown"))
))),
}
})
self.save(&ptr)?;
});
Ok(())
}

/// Revert changes to an option, section or package
Expand All @@ -333,7 +332,7 @@ impl Uci {
pub fn revert(&mut self, identifier: &str) -> Result<()> {
libuci_locked!(self, {
let mut ptr = self.get_ptr(identifier)?;
let result = unsafe { uci_revert(self.ctx, &mut ptr.0) };
let result = unsafe { uci_revert(self.ctx, ptr.deref_mut()) };
if result != UCI_OK {
return Err(Error::Message(format!(
"Could not revert uci key: {}, {}, {}",
Expand All @@ -343,18 +342,7 @@ impl Uci {
.unwrap_or_else(|_| String::from("Unknown"))
)));
}
let result = unsafe { uci_save(self.ctx, ptr.p) };
if result == UCI_OK {
Ok(())
} else {
Err(Error::Message(format!(
"Could not save uci key: {}, {}, {}",
identifier,
result,
self.get_last_error()
.unwrap_or_else(|_| String::from("Unknown"))
)))
}
self.save(&ptr)
})
}

Expand All @@ -372,14 +360,15 @@ impl Uci {
)));
}
libuci_locked!(self, {
let mut ptr = self.get_ptr(format!("{}={}", identifier, val).as_ref())?;
let assignment = format!("{}={}", identifier, val);
let mut ptr = self.get_ptr(&assignment)?;
if ptr.value.is_null() {
return Err(Error::Message(format!(
"parsed value is null: {}={}",
identifier, val
)));
}
let result = unsafe { uci_set(self.ctx, &mut ptr.0) };
let result = unsafe { uci_set(self.ctx, ptr.deref_mut()) };
if result != UCI_OK {
return Err(Error::Message(format!(
"Could not set uci key: {}={}, {}, {}",
Expand All @@ -390,18 +379,68 @@ impl Uci {
.unwrap_or_else(|_| String::from("Unknown"))
)));
}
self.save(&ptr)
})
}

/// Append a string to an element list in UCI. identifier and value will be concatenated to the
/// following assignment: `{identifier}={val}`.
///
/// If the given option already contains a string value, it will be converted to an 1-element-list before appending the next element.
///
/// If the addition failed an `Err` is returned.
pub fn add_list(&mut self, identifier: &str, val: &str) -> Result<()> {
if val.contains('\'') {
return Err(Error::Message(format!(
"Values may not contain quotes: {}={}",
identifier, val
)));
}
libuci_locked!(self, {
let assignment = format!("{}={}", identifier, val);
let mut ptr = self.get_ptr(identifier)?.assert_value_set()?;
// Safety:
// * self.ctx points to a valid UCI context.
// * ptr is valid because self.get_ptr returned Ok(ptr).
let result = unsafe { uci_add_list(self.ctx, ptr.deref_mut()) };
if result != UCI_OK {
return Err(Error::Message(format!(
"Could not append string to element list: {}, {}, {}",
assignment,
result,
self.get_last_error()
.unwrap_or_else(|_| String::from("Unknown"))
)));
}
self.save(&ptr)?;
});
Ok(())
}

/// Save change delta for the specified package referenced by `ptr`. See [Self::get_ptr].
///
/// UCI will keep the delta changes in a temporary location until `uci_commit()` or `uci_revert()` is called.
/// The change delta may be applied (and flushed) by calling [Self::commit].
fn save(&mut self, ptr: &UciPtr) -> Result<()> {
let identifier = ptr._key.to_string_lossy().to_string();
libuci_locked!(self, {
// Safety:
// * self.ctx points to a valid UCI context.
// * ptr was constructed using Self::get_ptr, which guarantees that the pointer is
// valid.
let result = unsafe { uci_save(self.ctx, ptr.p) };
if result == UCI_OK {
Ok(())
} else {
Err(Error::Message(format!(
"Could not save uci key: {}={}, {}, {}",
match result {
UCI_OK => Ok(()),
UCI_ERR_NOTFOUND => Err(Error::EntryNotFound {
entry_identifier: identifier,
}),
_ => Err(Error::Message(format!(
"Could not save uci package: {}, {}, {}",
identifier,
val,
result,
self.get_last_error()
.unwrap_or_else(|_| String::from("Unknown"))
)))
))),
}
})
}
Expand Down Expand Up @@ -515,11 +554,14 @@ impl Uci {
option: ptr::null(),
value: ptr::null(),
};
let raw = libuci_locked!(self, {
let key = libuci_locked!(self, {
let raw = CString::new(identifier)?.into_raw();
let result = unsafe { uci_lookup_ptr(self.ctx, &mut ptr, raw, true) };
match result {
UCI_OK => (),
// Safety: raw was created from CString::into_raw.
// raw is not aliased / referenced from anywhere else for the lifetime of the
// reconstructed CString.
UCI_OK => unsafe { CString::from_raw(raw) },
UCI_ERR_NOTFOUND => {
return Err(Error::EntryNotFound {
entry_identifier: identifier.to_string(),
Expand All @@ -535,11 +577,10 @@ impl Uci {
)));
}
}
raw
});
debug!("{:?}", ptr);
if !ptr.last.is_null() {
Ok(UciPtr(ptr, raw))
Ok(UciPtr { ptr, _key: key })
} else {
Err(Error::Message(format!(
"Cannot access null value: {}",
Expand Down Expand Up @@ -629,8 +670,8 @@ mod tests {
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())?;
uci.set_config_dir(&config_dir)?;
uci.set_save_dir(&save_dir)?;
Ok((uci, tmp))
}

Expand Down