rust_uci/
lib.rs

1// Copyright 2021, Benjamin Ludewig
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Bindings to OpenWRT UCI
5//!
6//! This crate provides a safe interface to OpenWRT's Unified Configuration Interface C-Library.
7//!
8//! # Building
9//!
10//! Both UCI libraries and headers are required to build this crate. There are multiple options available to locate
11//! UCI.
12//!
13//! ## Inside OpenWRT SDK
14//!
15//! If building inside the OpenWRT SDK with OpenWRT's UCI package set the environment variable
16//! `UCI_DIR=$(STAGING_DIR)/usr` using the corresponding Makefile.
17//! rust-uci will automatically use the headers and libraries for the target system.
18//!
19//! ## Vendored
20//!
21//! If no `UCI_DIR` variable is set, rust-uci will compile against the distributed libuci source files licensed under GPLv2.
22//!
23//! # Example Usage
24//!
25//! ```no_run
26//! use rust_uci::Uci;
27//!
28//! let mut uci = Uci::new()?;
29//! // Get type of a section
30//! assert_eq!(uci.get("network.wan")?, "interface");
31//! // Get value of an option, UCI's extended syntax is supported
32//! assert_eq!(uci.get("network.@interface[0].proto")?, "static");
33//! assert_eq!(uci.get("network.lan.proto")?, "static");
34//!
35//! // Create a new section
36//! uci.set("network.newnet", "interface")?;
37//! uci.set("network.newnet.proto", "static")?;
38//! uci.set("network.newnet.ifname", "en0")?;
39//! uci.set("network.newnet.enabled", "1")?;
40//! uci.set("network.newnet.ipaddr", "2.3.4.5")?;
41//! uci.set("network.newnet.test", "123")?;
42//! uci.delete("network.newnet.test")?;
43//! // IMPORTANT: Commit or revert the changes
44//! uci.commit("network")?;
45//! uci.revert("network")?;
46//!
47//! ```
48
49pub mod error;
50
51use core::ptr;
52use std::{
53    ffi::{CStr, CString},
54    ops::{Deref, DerefMut},
55};
56
57use libuci_sys::{
58    uci_alloc_context, uci_commit, uci_context, uci_delete, uci_free_context, uci_get_errorstr,
59    uci_lookup_ptr, uci_option_type_UCI_TYPE_STRING, uci_ptr, uci_ptr_UCI_LOOKUP_COMPLETE,
60    uci_revert, uci_save, uci_set, uci_set_confdir, uci_set_savedir, uci_type_UCI_TYPE_OPTION,
61    uci_type_UCI_TYPE_SECTION, uci_unload,
62};
63use log::debug;
64
65use crate::error::{Error, Result};
66
67#[allow(clippy::cast_possible_wrap)]
68const UCI_OK: i32 = libuci_sys::UCI_OK as i32;
69
70/// Contains the native `uci_context`
71pub struct Uci(*mut uci_context);
72
73impl Drop for Uci {
74    fn drop(&mut self) {
75        unsafe { uci_free_context(self.0) }
76    }
77}
78
79/// Contains the native `uci_ptr` and it's raw `CString` key
80/// this is done so the raw `CString` stays alive until the `uci_ptr` is dropped
81struct UciPtr(uci_ptr, *mut std::os::raw::c_char);
82
83impl Deref for UciPtr {
84    type Target = uci_ptr;
85
86    fn deref(&self) -> &Self::Target {
87        &self.0
88    }
89}
90
91impl DerefMut for UciPtr {
92    fn deref_mut(&mut self) -> &mut Self::Target {
93        &mut self.0
94    }
95}
96
97impl Drop for UciPtr {
98    fn drop(&mut self) {
99        unsafe { CString::from_raw(self.1) };
100    }
101}
102
103impl Uci {
104    /// Creates a new UCI context.
105    /// The C memory will be freed when the object is dropped.
106    pub fn new() -> Result<Uci> {
107        let ctx = unsafe { uci_alloc_context() };
108        if !ctx.is_null() {
109            Ok(Uci(ctx))
110        } else {
111            Err(Error::Message(String::from("Could not alloc uci context")))
112        }
113    }
114
115    /// Sets the config directory of UCI, this is `/etc/config` by default.
116    pub fn set_config_dir(&mut self, config_dir: &str) -> Result<()> {
117        let result = unsafe {
118            let raw = CString::new(config_dir)?;
119            uci_set_confdir(
120                self.0,
121                raw.as_bytes_with_nul()
122                    .as_ptr()
123                    .cast::<std::os::raw::c_char>(),
124            )
125        };
126        if result == UCI_OK {
127            debug!("Set config dir to: {}", config_dir);
128            Ok(())
129        } else {
130            Err(Error::Message(format!(
131                "Cannot set config dir: {}, {}",
132                config_dir,
133                self.get_last_error()
134                    .unwrap_or_else(|_| String::from("Unknown"))
135            )))
136        }
137    }
138
139    /// Sets the save directory of UCI, this is `/tmp/.uci` by default.
140    pub fn set_save_dir(&mut self, save_dir: &str) -> Result<()> {
141        let result = unsafe {
142            let raw = CString::new(save_dir)?;
143            uci_set_savedir(
144                self.0,
145                raw.as_bytes_with_nul()
146                    .as_ptr()
147                    .cast::<std::os::raw::c_char>(),
148            )
149        };
150        if result == UCI_OK {
151            debug!("Set save dir to: {}", save_dir);
152            Ok(())
153        } else {
154            Err(Error::Message(format!(
155                "Cannot set save dir: {}, {}",
156                save_dir,
157                self.get_last_error()
158                    .unwrap_or_else(|_| String::from("Unknown"))
159            )))
160        }
161    }
162
163    /// Delete an option or section in UCI.
164    /// UCI will keep the delta changes in a temporary location until `commit()` or `revert()` is called.
165    ///
166    /// Allowed keys are like `network.wan.proto`, `network.@interface[-1].iface`, `network.wan` and `network.@interface[-1]`
167    ///
168    /// if the deletion failed an `Err` is returned.
169    pub fn delete(&mut self, identifier: &str) -> Result<()> {
170        let mut ptr = self.get_ptr(identifier)?;
171        let result = unsafe { uci_delete(self.0, &mut ptr.0) };
172        if result != UCI_OK {
173            return Err(Error::Message(format!(
174                "Could not delete uci key: {}, {}, {}",
175                identifier,
176                result,
177                self.get_last_error()
178                    .unwrap_or_else(|_| String::from("Unknown"))
179            )));
180        }
181        let result = unsafe { uci_save(self.0, ptr.p) };
182        if result == UCI_OK {
183            Ok(())
184        } else {
185            Err(Error::Message(format!(
186                "Could not save uci key: {}, {}, {}",
187                identifier,
188                result,
189                self.get_last_error()
190                    .unwrap_or_else(|_| String::from("Unknown"))
191            )))
192        }
193    }
194
195    /// Revert changes to an option, section or package
196    ///
197    /// Allowed keys are like `network`, `network.wan.proto`, `network.@interface[-1].iface`, `network.wan` and `network.@interface[-1]`
198    ///
199    /// if the deletion failed an `Err` is returned.
200    pub fn revert(&mut self, identifier: &str) -> Result<()> {
201        let mut ptr = self.get_ptr(identifier)?;
202        let result = unsafe { uci_revert(self.0, &mut ptr.0) };
203        if result != UCI_OK {
204            return Err(Error::Message(format!(
205                "Could not revert uci key: {}, {}, {}",
206                identifier,
207                result,
208                self.get_last_error()
209                    .unwrap_or_else(|_| String::from("Unknown"))
210            )));
211        }
212        let result = unsafe { uci_save(self.0, ptr.p) };
213        if result == UCI_OK {
214            Ok(())
215        } else {
216            Err(Error::Message(format!(
217                "Could not save uci key: {}, {}, {}",
218                identifier,
219                result,
220                self.get_last_error()
221                    .unwrap_or_else(|_| String::from("Unknown"))
222            )))
223        }
224    }
225
226    /// Sets an option value or section type in UCI, creates the key if necessary.
227    /// UCI will keep the delta changes in a temporary location until `commit()` or `revert()` is called.
228    ///
229    /// Allowed keys are like `network.wan.proto`, `network.@interface[-1].iface`, `network.wan` and `network.@interface[-1]`
230    ///
231    /// if the assignment failed an `Err` is returned.
232    pub fn set(&mut self, identifier: &str, val: &str) -> Result<()> {
233        if val.contains('\'') {
234            return Err(Error::Message(format!(
235                "Values may not contain quotes: {}={}",
236                identifier, val
237            )));
238        }
239        let mut ptr = self.get_ptr(format!("{}={}", identifier, val).as_ref())?;
240        if ptr.value.is_null() {
241            return Err(Error::Message(format!(
242                "parsed value is null: {}={}",
243                identifier, val
244            )));
245        }
246        let result = unsafe { uci_set(self.0, &mut ptr.0) };
247        if result != UCI_OK {
248            return Err(Error::Message(format!(
249                "Could not set uci key: {}={}, {}, {}",
250                identifier,
251                val,
252                result,
253                self.get_last_error()
254                    .unwrap_or_else(|_| String::from("Unknown"))
255            )));
256        }
257        let result = unsafe { uci_save(self.0, ptr.p) };
258        if result == UCI_OK {
259            Ok(())
260        } else {
261            Err(Error::Message(format!(
262                "Could not save uci key: {}={}, {}, {}",
263                identifier,
264                val,
265                result,
266                self.get_last_error()
267                    .unwrap_or_else(|_| String::from("Unknown"))
268            )))
269        }
270    }
271
272    /// Commit all changes to the specified package
273    /// writing the temporary delta to the config file
274    pub fn commit(&mut self, package: &str) -> Result<()> {
275        let mut ptr = self.get_ptr(package)?;
276        let result = unsafe { uci_commit(self.0, &mut ptr.p, false) };
277        if result != UCI_OK {
278            return Err(Error::Message(format!(
279                "Could not set commit uci package: {}, {}, {}",
280                package,
281                result,
282                self.get_last_error()
283                    .unwrap_or_else(|_| String::from("Unknown"))
284            )));
285        }
286        if !ptr.p.is_null() {
287            unsafe {
288                uci_unload(self.0, ptr.p);
289            }
290        }
291        Ok(())
292    }
293
294    /// Queries an option value or section type from UCI.
295    /// If a key has been changed in the delta, the updated value will be returned.
296    ///
297    /// Allowed keys are like `network.wan.proto`, `network.@interface[-1].iface`, `network.lan` and `network.@interface[-1]`
298    ///
299    /// if the entry does not exist an `Err` is returned.
300    pub fn get(&mut self, key: &str) -> Result<String> {
301        let ptr = self.get_ptr(key)?;
302        if ptr.flags & uci_ptr_UCI_LOOKUP_COMPLETE == 0 {
303            return Err(Error::Message(format!("Lookup failed: {}", key)));
304        }
305        let last = unsafe { *ptr.last };
306        #[allow(non_upper_case_globals)]
307        match last.type_ {
308            uci_type_UCI_TYPE_OPTION => {
309                let opt = unsafe { *ptr.o };
310                if opt.type_ != uci_option_type_UCI_TYPE_STRING {
311                    return Err(Error::Message(format!(
312                        "Cannot get string value of non-string: {} {}",
313                        key, opt.type_
314                    )));
315                }
316                if opt.section.is_null() {
317                    return Err(Error::Message(format!("uci section was null: {}", key)));
318                }
319                let sect = unsafe { *opt.section };
320                if sect.package.is_null() {
321                    return Err(Error::Message(format!("uci package was null: {}", key)));
322                }
323                let pack = unsafe { *sect.package };
324                let value = unsafe { CStr::from_ptr(opt.v.string).to_str()? };
325
326                debug!(
327                    "{}.{}.{}={}",
328                    unsafe { CStr::from_ptr(pack.e.name) }.to_str()?,
329                    unsafe { CStr::from_ptr(sect.e.name) }.to_str()?,
330                    unsafe { CStr::from_ptr(opt.e.name) }.to_str()?,
331                    value
332                );
333                Ok(String::from(value))
334            }
335            uci_type_UCI_TYPE_SECTION => {
336                let sect = unsafe { *ptr.s };
337                if sect.package.is_null() {
338                    return Err(Error::Message(format!("uci package was null: {}", key)));
339                }
340                let pack = unsafe { *sect.package };
341                let typ = unsafe { CStr::from_ptr(sect.type_).to_str()? };
342
343                debug!(
344                    "{}.{}={}",
345                    unsafe { CStr::from_ptr(pack.e.name) }.to_str()?,
346                    unsafe { CStr::from_ptr(sect.e.name) }.to_str()?,
347                    typ
348                );
349                Ok(String::from(typ))
350            }
351            _ => return Err(Error::Message(format!("unsupported type: {}", last.type_))),
352        }
353    }
354
355    /// Queries UCI (e.g. `package.section.key`)
356    ///
357    /// This also supports advanced syntax like `network.@interface[-1].ifname` (get ifname of last interface)
358    ///
359    /// An `Ok(result)` is guaranteed to be a valid ptr and ptr.last will be set.
360    ///
361    /// If the key could not be found `ptr.flags & UCI_LOOKUP_COMPLETE` will not be set, but the ptr is still valid.
362    ///
363    /// If `identifier` is assignment like `network.wan.proto="dhcp"`, `ptr.value` will be set.
364    fn get_ptr(&mut self, identifier: &str) -> Result<UciPtr> {
365        let mut ptr = uci_ptr {
366            target: 0,
367            flags: 0,
368            p: ptr::null_mut(),
369            s: ptr::null_mut(),
370            o: ptr::null_mut(),
371            last: ptr::null_mut(),
372            package: ptr::null(),
373            section: ptr::null(),
374            option: ptr::null(),
375            value: ptr::null(),
376        };
377        let raw = CString::new(identifier)?.into_raw();
378        let result = unsafe { uci_lookup_ptr(self.0, &mut ptr, raw, true) };
379        if result != UCI_OK {
380            return Err(Error::Message(format!(
381                "Could not parse uci key: {}, {}, {}",
382                identifier,
383                result,
384                self.get_last_error()
385                    .unwrap_or_else(|_| String::from("Unknown"))
386            )));
387        }
388        debug!("{:?}", ptr);
389        if !ptr.last.is_null() {
390            Ok(UciPtr(ptr, raw))
391        } else {
392            Err(Error::Message(format!(
393                "Cannot access null value: {}",
394                identifier
395            )))
396        }
397    }
398
399    /// Obtains the most recent error from UCI as a string
400    /// if no `last_error` is set, an `Err` is returned.
401    fn get_last_error(&mut self) -> Result<String> {
402        let mut raw: *mut std::os::raw::c_char = ptr::null_mut();
403        unsafe { uci_get_errorstr(self.0, &mut raw, ptr::null()) };
404        if raw.is_null() {
405            return Err(Error::Message(String::from("last_error was null")));
406        }
407        match unsafe { CStr::from_ptr(raw) }.to_str() {
408            Ok(o) => {
409                let s = String::from(o);
410                unsafe { libc::free(raw.cast::<std::os::raw::c_void>()) };
411                Ok(s)
412            }
413            Err(e) => {
414                unsafe { libc::free(raw.cast::<std::os::raw::c_void>()) };
415                Err(e.into())
416            }
417        }
418    }
419}