rust-uci 0.1.5

OpenWRT UCI rust bindings
Documentation
// Copyright 2021, Benjamin Ludewig
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Bindings to OpenWRT UCI
//!
//! This crate provides a safe interface to OpenWRT's Unified Configuration Interface C-Library.
//!
//! # Building
//!
//! Both UCI libraries and headers are required to build this crate. There are multiple options available to locate
//! UCI.
//!
//! ## Inside OpenWRT SDK
//!
//! If building inside the OpenWRT SDK with OpenWRT's UCI package set the environment variable
//! `UCI_DIR=$(STAGING_DIR)/usr` using the corresponding Makefile.
//! rust-uci will automatically use the headers and libraries for the target system.
//!
//! ## Vendored
//!
//! If no `UCI_DIR` variable is set, rust-uci will compile against the distributed libuci source files licensed under GPLv2.
//!
//! # Example Usage
//!
//! ```no_run
//! use rust_uci::Uci;
//!
//! let mut uci = Uci::new()?;
//! // Get type of a section
//! assert_eq!(uci.get("network.wan")?, "interface");
//! // Get value of an option, UCI's extended syntax is supported
//! assert_eq!(uci.get("network.@interface[0].proto")?, "static");
//! assert_eq!(uci.get("network.lan.proto")?, "static");
//!
//! // Create a new section
//! uci.set("network.newnet", "interface")?;
//! uci.set("network.newnet.proto", "static")?;
//! uci.set("network.newnet.ifname", "en0")?;
//! uci.set("network.newnet.enabled", "1")?;
//! uci.set("network.newnet.ipaddr", "2.3.4.5")?;
//! uci.set("network.newnet.test", "123")?;
//! uci.delete("network.newnet.test")?;
//! // IMPORTANT: Commit or revert the changes
//! uci.commit("network")?;
//! uci.revert("network")?;
//!
//! ```

pub mod error;

use core::ptr;
use std::{
    ffi::{CStr, CString},
    ops::{Deref, DerefMut},
};

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_type_UCI_TYPE_SECTION, uci_unload,
};
use log::debug;

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

#[allow(clippy::cast_possible_wrap)]
const UCI_OK: i32 = libuci_sys::UCI_OK as i32;

/// Contains the native `uci_context`
pub struct Uci(*mut uci_context);

impl Drop for Uci {
    fn drop(&mut self) {
        unsafe { uci_free_context(self.0) }
    }
}

/// 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);

impl Deref for UciPtr {
    type Target = uci_ptr;

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

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

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

impl Uci {
    /// Creates a new UCI context.
    /// The C memory will be freed when the object is dropped.
    pub fn new() -> Result<Uci> {
        let ctx = unsafe { uci_alloc_context() };
        if !ctx.is_null() {
            Ok(Uci(ctx))
        } else {
            Err(Error::Message(String::from("Could not alloc uci context")))
        }
    }

    /// Sets the config directory of UCI, this is `/etc/config` by default.
    pub fn set_config_dir(&mut self, config_dir: &str) -> Result<()> {
        let result = unsafe {
            let raw = CString::new(config_dir)?;
            uci_set_confdir(
                self.0,
                raw.as_bytes_with_nul()
                    .as_ptr()
                    .cast::<std::os::raw::c_char>(),
            )
        };
        if result == UCI_OK {
            debug!("Set config dir to: {}", config_dir);
            Ok(())
        } else {
            Err(Error::Message(format!(
                "Cannot set config dir: {}, {}",
                config_dir,
                self.get_last_error()
                    .unwrap_or_else(|_| String::from("Unknown"))
            )))
        }
    }

    /// Sets the save directory of UCI, this is `/tmp/.uci` by default.
    pub fn set_save_dir(&mut self, save_dir: &str) -> Result<()> {
        let result = unsafe {
            let raw = CString::new(save_dir)?;
            uci_set_savedir(
                self.0,
                raw.as_bytes_with_nul()
                    .as_ptr()
                    .cast::<std::os::raw::c_char>(),
            )
        };
        if result == UCI_OK {
            debug!("Set save dir to: {}", save_dir);
            Ok(())
        } else {
            Err(Error::Message(format!(
                "Cannot set save dir: {}, {}",
                save_dir,
                self.get_last_error()
                    .unwrap_or_else(|_| String::from("Unknown"))
            )))
        }
    }

    /// Delete an option or section in UCI.
    /// UCI will keep the delta changes in a temporary location until `commit()` or `revert()` is called.
    ///
    /// Allowed keys are like `network.wan.proto`, `network.@interface[-1].iface`, `network.wan` and `network.@interface[-1]`
    ///
    /// if the deletion failed an `Err` is returned.
    pub fn delete(&mut self, identifier: &str) -> Result<()> {
        let mut ptr = self.get_ptr(identifier)?;
        let result = unsafe { uci_delete(self.0, &mut ptr.0) };
        if result != UCI_OK {
            return Err(Error::Message(format!(
                "Could not delete uci key: {}, {}, {}",
                identifier,
                result,
                self.get_last_error()
                    .unwrap_or_else(|_| String::from("Unknown"))
            )));
        }
        let result = unsafe { uci_save(self.0, 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"))
            )))
        }
    }

    /// Revert changes to an option, section or package
    ///
    /// Allowed keys are like `network`, `network.wan.proto`, `network.@interface[-1].iface`, `network.wan` and `network.@interface[-1]`
    ///
    /// if the deletion failed an `Err` is returned.
    pub fn revert(&mut self, identifier: &str) -> Result<()> {
        let mut ptr = self.get_ptr(identifier)?;
        let result = unsafe { uci_revert(self.0, &mut ptr.0) };
        if result != UCI_OK {
            return Err(Error::Message(format!(
                "Could not revert uci key: {}, {}, {}",
                identifier,
                result,
                self.get_last_error()
                    .unwrap_or_else(|_| String::from("Unknown"))
            )));
        }
        let result = unsafe { uci_save(self.0, 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"))
            )))
        }
    }

    /// Sets an option value or section type in UCI, creates the key if necessary.
    /// UCI will keep the delta changes in a temporary location until `commit()` or `revert()` is called.
    ///
    /// Allowed keys are like `network.wan.proto`, `network.@interface[-1].iface`, `network.wan` and `network.@interface[-1]`
    ///
    /// if the assignment failed an `Err` is returned.
    pub fn set(&mut self, identifier: &str, val: &str) -> Result<()> {
        if val.contains('\'') {
            return Err(Error::Message(format!(
                "Values may not contain quotes: {}={}",
                identifier, val
            )));
        }
        let mut ptr = self.get_ptr(format!("{}={}", identifier, val).as_ref())?;
        if ptr.value.is_null() {
            return Err(Error::Message(format!(
                "parsed value is null: {}={}",
                identifier, val
            )));
        }
        let result = unsafe { uci_set(self.0, &mut ptr.0) };
        if result != UCI_OK {
            return Err(Error::Message(format!(
                "Could not set uci key: {}={}, {}, {}",
                identifier,
                val,
                result,
                self.get_last_error()
                    .unwrap_or_else(|_| String::from("Unknown"))
            )));
        }
        let result = unsafe { uci_save(self.0, ptr.p) };
        if result == UCI_OK {
            Ok(())
        } else {
            Err(Error::Message(format!(
                "Could not save uci key: {}={}, {}, {}",
                identifier,
                val,
                result,
                self.get_last_error()
                    .unwrap_or_else(|_| String::from("Unknown"))
            )))
        }
    }

    /// Commit all changes to the specified package
    /// writing the temporary delta to the config file
    pub fn commit(&mut self, package: &str) -> Result<()> {
        let mut ptr = self.get_ptr(package)?;
        let result = unsafe { uci_commit(self.0, &mut ptr.p, false) };
        if result != UCI_OK {
            return Err(Error::Message(format!(
                "Could not set commit uci package: {}, {}, {}",
                package,
                result,
                self.get_last_error()
                    .unwrap_or_else(|_| String::from("Unknown"))
            )));
        }
        if !ptr.p.is_null() {
            unsafe {
                uci_unload(self.0, ptr.p);
            }
        }
        Ok(())
    }

    /// Queries an option value or section type from UCI.
    /// If a key has been changed in the delta, the updated value will be returned.
    ///
    /// Allowed keys are like `network.wan.proto`, `network.@interface[-1].iface`, `network.lan` and `network.@interface[-1]`
    ///
    /// if the entry does not exist an `Err` is returned.
    pub fn get(&mut self, key: &str) -> Result<String> {
        let ptr = self.get_ptr(key)?;
        if ptr.flags & uci_ptr_UCI_LOOKUP_COMPLETE == 0 {
            return Err(Error::Message(format!("Lookup failed: {}", key)));
        }
        let last = unsafe { *ptr.last };
        #[allow(non_upper_case_globals)]
        match last.type_ {
            uci_type_UCI_TYPE_OPTION => {
                let opt = unsafe { *ptr.o };
                if opt.type_ != uci_option_type_UCI_TYPE_STRING {
                    return Err(Error::Message(format!(
                        "Cannot get string value of non-string: {} {}",
                        key, opt.type_
                    )));
                }
                if opt.section.is_null() {
                    return Err(Error::Message(format!("uci section was null: {}", key)));
                }
                let sect = unsafe { *opt.section };
                if sect.package.is_null() {
                    return Err(Error::Message(format!("uci package was null: {}", key)));
                }
                let pack = unsafe { *sect.package };
                let value = unsafe { CStr::from_ptr(opt.v.string).to_str()? };

                debug!(
                    "{}.{}.{}={}",
                    unsafe { CStr::from_ptr(pack.e.name) }.to_str()?,
                    unsafe { CStr::from_ptr(sect.e.name) }.to_str()?,
                    unsafe { CStr::from_ptr(opt.e.name) }.to_str()?,
                    value
                );
                Ok(String::from(value))
            }
            uci_type_UCI_TYPE_SECTION => {
                let sect = unsafe { *ptr.s };
                if sect.package.is_null() {
                    return Err(Error::Message(format!("uci package was null: {}", key)));
                }
                let pack = unsafe { *sect.package };
                let typ = unsafe { CStr::from_ptr(sect.type_).to_str()? };

                debug!(
                    "{}.{}={}",
                    unsafe { CStr::from_ptr(pack.e.name) }.to_str()?,
                    unsafe { CStr::from_ptr(sect.e.name) }.to_str()?,
                    typ
                );
                Ok(String::from(typ))
            }
            _ => return Err(Error::Message(format!("unsupported type: {}", last.type_))),
        }
    }

    /// Queries UCI (e.g. `package.section.key`)
    ///
    /// This also supports advanced syntax like `network.@interface[-1].ifname` (get ifname of last interface)
    ///
    /// An `Ok(result)` is guaranteed to be a valid ptr and ptr.last will be set.
    ///
    /// If the key could not be found `ptr.flags & UCI_LOOKUP_COMPLETE` will not be set, but the ptr is still valid.
    ///
    /// If `identifier` is assignment like `network.wan.proto="dhcp"`, `ptr.value` will be set.
    fn get_ptr(&mut self, identifier: &str) -> Result<UciPtr> {
        let mut ptr = uci_ptr {
            target: 0,
            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(),
        };
        let raw = CString::new(identifier)?.into_raw();
        let result = unsafe { uci_lookup_ptr(self.0, &mut ptr, raw, true) };
        if result != UCI_OK {
            return Err(Error::Message(format!(
                "Could not parse uci key: {}, {}, {}",
                identifier,
                result,
                self.get_last_error()
                    .unwrap_or_else(|_| String::from("Unknown"))
            )));
        }
        debug!("{:?}", ptr);
        if !ptr.last.is_null() {
            Ok(UciPtr(ptr, raw))
        } else {
            Err(Error::Message(format!(
                "Cannot access null value: {}",
                identifier
            )))
        }
    }

    /// 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> {
        let mut raw: *mut std::os::raw::c_char = ptr::null_mut();
        unsafe { uci_get_errorstr(self.0, &mut raw, ptr::null()) };
        if raw.is_null() {
            return Err(Error::Message(String::from("last_error was null")));
        }
        match unsafe { CStr::from_ptr(raw) }.to_str() {
            Ok(o) => {
                let s = String::from(o);
                unsafe { libc::free(raw.cast::<std::os::raw::c_void>()) };
                Ok(s)
            }
            Err(e) => {
                unsafe { libc::free(raw.cast::<std::os::raw::c_void>()) };
                Err(e.into())
            }
        }
    }
}