libnftables1-sys 1.0.0

FFI bindings for libnftables1.
mod bindings;

use std::os::raw::{c_char, c_uint};

pub use crate::bindings::*;

pub struct Nftables {
    ctx: *mut nft_ctx,
}

impl Default for Nftables {
    fn default() -> Self {
        Self::new()
    }
}

impl Nftables {
    pub fn new() -> Nftables {
        let ctx = unsafe { nft_ctx_new(0) };
        unsafe { nft_ctx_buffer_output(ctx) };
        unsafe { nft_ctx_buffer_error(ctx) };
        Nftables { ctx }
    }

    #[allow(clippy::not_unsafe_ptr_arg_deref)] // Clippy complained about cmd perhaps being null,
                                               // despite check
    pub fn run_cmd(&mut self, cmd: *const c_char) -> (i32, *const c_char, *const c_char) {
        assert_ne!(self.ctx, ::std::ptr::null_mut());
        if cmd.is_null() {
            return (-1, ::std::ptr::null(), ::std::ptr::null());
        }

        let rc = unsafe { nft_run_cmd_from_buffer(self.ctx, cmd) };
        let output = unsafe { nft_ctx_get_output_buffer(self.ctx) };
        let error = unsafe { nft_ctx_get_error_buffer(self.ctx) };

        (rc, output, error)
    }

    pub fn set_debug(&mut self, flags: nft_debug_level) {
        unsafe { nft_ctx_output_set_debug(self.ctx, flags) };
    }

    pub fn get_debug(&self) -> nft_debug_level {
        unsafe { nft_ctx_output_get_debug(self.ctx) }
    }

    fn set_flags(&mut self, flags: c_uint) {
        let old_flags = unsafe { nft_ctx_output_get_flags(self.ctx) };
        unsafe { nft_ctx_output_set_flags(self.ctx, old_flags | flags) };
    }

    pub fn set_output_handle(&mut self) {
        self.set_flags(NFT_CTX_OUTPUT_HANDLE);
    }

    #[deprecated(
        since = "1.0.0",
        note = "please use `set_output_numeric_time()` instead"
    )]
    pub fn set_numeric_time(&mut self) {
        self.set_flags(NFT_CTX_OUTPUT_NUMERIC_TIME);
    }

    pub fn set_output_numeric_time(&mut self) {
        self.set_flags(NFT_CTX_OUTPUT_NUMERIC_TIME);
    }

    pub fn set_output_json(&mut self) {
        self.set_flags(NFT_CTX_OUTPUT_JSON);
    }
}

impl Drop for Nftables {
    fn drop(&mut self) {
        unsafe { nft_ctx_free(self.ctx) };
        self.ctx = ::std::ptr::null_mut();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::ffi::CStr;

    extern "C" {
        fn getuid() -> u32;
    }

    #[test]
    fn list_ruleset() {
        assert_eq!(unsafe { getuid() }, 0);

        let mut nft = Nftables::new();

        assert!(nft.ctx != ::std::ptr::null_mut());

        let cmd = CStr::from_bytes_with_nul(b"list ruleset\0").unwrap();
        let (rc, output, error) = nft.run_cmd(cmd.as_ptr());

        assert_eq!(rc, 0);
        assert_ne!(output, ::std::ptr::null());
        assert_ne!(error, ::std::ptr::null());
    }

    #[test]
    fn set_debug() {
        // assert_eq!(unsafe{ getuid() }, 0);

        let mut nft = Nftables::new();

        assert!(nft.ctx != ::std::ptr::null_mut());

        let lvl = nft.get_debug();
        assert_eq!(lvl, 0);

        nft.set_debug(NFT_DEBUG_SCANNER | NFT_DEBUG_EVALUATION | NFT_DEBUG_NETLINK);

        let lvl = nft.get_debug();
        assert_eq!(lvl, 0xd);
    }

    #[test]
    fn set_output_flags() {
        let mut nft = Nftables::new();

        assert!(nft.ctx != ::std::ptr::null_mut());

        // Check initial flags are 0
        let initial_flags = unsafe { nft_ctx_output_get_flags(nft.ctx) };
        assert_eq!(initial_flags, 0);

        // Set JSON flag
        nft.set_output_json();
        let flags = unsafe { nft_ctx_output_get_flags(nft.ctx) };
        assert_eq!(flags & NFT_CTX_OUTPUT_JSON, NFT_CTX_OUTPUT_JSON);

        // Set handle flag (should be additive)
        nft.set_output_handle();
        let flags = unsafe { nft_ctx_output_get_flags(nft.ctx) };
        assert_eq!(flags & NFT_CTX_OUTPUT_JSON, NFT_CTX_OUTPUT_JSON);
        assert_eq!(flags & NFT_CTX_OUTPUT_HANDLE, NFT_CTX_OUTPUT_HANDLE);

        // Set numeric time flag (should be additive)
        nft.set_numeric_time();
        let flags = unsafe { nft_ctx_output_get_flags(nft.ctx) };
        assert_eq!(flags & NFT_CTX_OUTPUT_JSON, NFT_CTX_OUTPUT_JSON);
        assert_eq!(flags & NFT_CTX_OUTPUT_HANDLE, NFT_CTX_OUTPUT_HANDLE);
        assert_eq!(
            flags & NFT_CTX_OUTPUT_NUMERIC_TIME,
            NFT_CTX_OUTPUT_NUMERIC_TIME
        );
    }

    #[test]
    fn set_output_json_output() {
        assert_eq!(unsafe { getuid() }, 0);

        let mut nft = Nftables::new();

        assert!(nft.ctx != ::std::ptr::null_mut());

        // Enable JSON output
        nft.set_output_json();

        // Run a simple command and verify JSON format in output
        let cmd = CStr::from_bytes_with_nul(b"list ruleset\0").unwrap();
        let (rc, output, _error) = nft.run_cmd(cmd.as_ptr());

        assert_eq!(rc, 0);
        assert_ne!(output, ::std::ptr::null());

        // Convert output to Rust string to check for JSON format
        let output_str = unsafe { std::ffi::CStr::from_ptr(output) }
            .to_str()
            .unwrap();

        // JSON output should start with '{' or be empty
        if !output_str.is_empty() {
            assert!(
                output_str.trim_start().starts_with('{'),
                "Expected JSON output to start with '{{', got: {}",
                &output_str[..output_str.len().min(100)]
            );
        }
    }
}