From 0237401e6ba2db0810a1f21e324e302fa8fe24f7 Mon Sep 17 00:00:00 2001 From: Grzegorz Nosek Date: Wed, 20 Mar 2024 11:58:03 +0100 Subject: [PATCH] new(tables): support tables with predefined fields --- Cargo.lock | 12 +- Cargo.toml | 2 +- falco_plugin/Cargo.toml | 3 +- falco_plugin/examples/parse_plugin.rs | 42 ++- falco_plugin/src/lib.rs | 59 ++++ .../src/plugin/exported_tables/mod.rs | 309 ++++++++++++++++-- .../src/plugin/exported_tables/wrappers.rs | 2 +- falco_plugin/src/plugin/tables/data.rs | 2 +- falco_plugin/src/plugin/tables/ffi.rs | 30 +- falco_plugin_derive/Cargo.toml | 18 + falco_plugin_derive/README.md | 5 + falco_plugin_derive/src/lib.rs | 137 ++++++++ 12 files changed, 574 insertions(+), 47 deletions(-) create mode 100644 falco_plugin_derive/Cargo.toml create mode 100644 falco_plugin_derive/README.md create mode 100644 falco_plugin_derive/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a9913191..b232d95c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,11 +65,12 @@ dependencies = [ [[package]] name = "falco_plugin" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "falco_event", "falco_plugin_api", + "falco_plugin_derive", "log", "memchr", "num-traits", @@ -83,6 +84,15 @@ dependencies = [ name = "falco_plugin_api" version = "0.1.0" +[[package]] +name = "falco_plugin_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "hexdump" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index f2bf5e28..8f5d226f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["falco_plugin_api", "falco_plugin", "falco_event_derive", "falco_event"] +members = ["falco_plugin_api", "falco_plugin", "falco_event_derive", "falco_event", "falco_plugin_derive"] resolver = "2" diff --git a/falco_plugin/Cargo.toml b/falco_plugin/Cargo.toml index 128e043d..7c3ef370 100644 --- a/falco_plugin/Cargo.toml +++ b/falco_plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "falco_plugin" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "Apache-2.0" description = "High level bindings for the Falco plugin API" @@ -36,6 +36,7 @@ crate_type = ["dylib"] thiserror = "1.0.58" falco_event = { path = "../falco_event", version = "0.1" } falco_plugin_api = { path = "../falco_plugin_api", version = "0.1" } +falco_plugin_derive = { path = "../falco_plugin_derive", version = "0.1" } serde = "1.0.197" serde_json = "1.0.114" schemars = "0.8.16" diff --git a/falco_plugin/examples/parse_plugin.rs b/falco_plugin/examples/parse_plugin.rs index 04753a4c..d49db46d 100644 --- a/falco_plugin/examples/parse_plugin.rs +++ b/falco_plugin/examples/parse_plugin.rs @@ -1,18 +1,46 @@ -use std::ffi::CStr; +use std::ffi::{CStr, CString}; use anyhow::anyhow; use falco_event::events::types::EventType; use falco_plugin::base::{Plugin, TableInitInput}; use falco_plugin::parse::{EventParseInput, ParsePlugin}; -use falco_plugin::tables::TypedTable; -use falco_plugin::tables::TypedTableField; +use falco_plugin::tables::{DynamicFieldValues, TypedTableField}; +use falco_plugin::tables::{DynamicTable, TypedTable}; use falco_plugin::{c, parse_plugin, plugin, EventInput, FailureReason}; use falco_plugin_api::{ss_plugin_event_input, ss_plugin_event_parse_input, ss_plugin_init_input}; +use falco_plugin_derive::TableValues; + +#[derive(TableValues, Default)] +struct AnotherTable { + #[readonly] + int_field: u64, + string_field: CString, + + #[hidden] + secret: Vec, + + #[dynamic] + dynamic_fields: DynamicFieldValues, +} + +#[derive(TableValues, Default)] +#[static_only] +struct TableWithStaticFieldsOnly { + #[readonly] + int_field: u64, + string_field: CString, + + #[hidden] + secret: Vec, +} pub struct DummyPlugin { thread_table: TypedTable, sample_field: TypedTableField, + new_table: &'static mut DynamicTable, + another_table: &'static mut DynamicTable, + table_with_static_fields_only: &'static mut DynamicTable, } impl Plugin for DummyPlugin { @@ -26,9 +54,17 @@ impl Plugin for DummyPlugin { let thread_table = input.get_table::(c!("threads"))?; let sample_field = thread_table.add_field::(c!("sample"))?; + let new_table = input.add_table(DynamicTable::new(c!("sample")))?; + let another_table = input.add_table(DynamicTable::new(c!("another")))?; + let table_with_static_fields_only = + input.add_table(DynamicTable::new(c!("static_fields_only")))?; + Ok(DummyPlugin { thread_table, sample_field, + new_table, + another_table, + table_with_static_fields_only, }) } } diff --git a/falco_plugin/src/lib.rs b/falco_plugin/src/lib.rs index 8d686601..0db136bf 100644 --- a/falco_plugin/src/lib.rs +++ b/falco_plugin/src/lib.rs @@ -3,9 +3,63 @@ #![deny(rustdoc::broken_intra_doc_links)] // reexport dependencies +pub use falco_plugin_api as api; pub use schemars; pub use serde; +/// Mark a struct type as a table value +/// +/// Tables in Falco plugins are effectively maps from a [key](`tables::TableData`) +/// to a (possibly dynamic) struct of values. +/// +/// The default implementation for tables ([`tables::DynamicFieldValues`]) uses +/// dynamic fields only, but with this macro you can also define structs containing static +/// (predefined) fields that are accessible to your plugin without going through the Falco +/// plugin API. +/// +/// A table can be fully static (no dynamic fields allowed). In this case, it must be tagged +/// with a `#[static_only]` attribute (to prevent accidental omission of the dynamic field values, +/// which would only get caught at runtime, possibly much later). +/// +/// Alternatively, it can mark a single field as `#[dynamic]`. That field needs to implement +/// [`tables::TableValues`] and it will generally be of type [`tables::DynamicFieldValues`]. +/// +/// Fields tagged as `#[readonly]` won't be writable via the Falco API and fields tagged +/// as `#[hidden]` won't be exposed to the API at all. This is useful if you want to store data +/// that's incompatible with the Falco plugin API in your table. +/// +/// # Example +/// ``` +/// use std::ffi::CString; +/// use falco_plugin::tables::DynamicFieldValues; +/// use falco_plugin::TableValues; +/// +/// #[derive(TableValues, Default)] // all table structs must implement Default +/// #[static_only] // no dynamic fields in this one +/// struct TableWithStaticFieldsOnly { +/// #[readonly] +/// int_field: u64, // this field cannot be modified with the Falco API +/// string_field: CString, +/// +/// #[hidden] +/// secret: Vec, // this field is not visible via the Falco API +/// } +/// +/// #[derive(TableValues, Default)] +/// struct AnotherTable { +/// #[readonly] +/// int_field: u64, // this field cannot be modified with the Falco API +/// string_field: CString, +/// +/// #[hidden] +/// secret: Vec, // this field is not visible via the Falco API +/// +/// #[dynamic] +/// dynamic_fields: DynamicFieldValues, // dynamically added fields have their values here +/// } +/// ``` +pub use falco_plugin_derive::TableValues; + pub use crate::plugin::error::FailureReason; pub use crate::plugin::event::EventInput; @@ -440,8 +494,12 @@ pub mod source { pub mod tables { pub use crate::plugin::exported_tables::DynamicField; pub use crate::plugin::exported_tables::DynamicFieldValue; + pub use crate::plugin::exported_tables::DynamicFieldValues; pub use crate::plugin::exported_tables::DynamicTable; pub use crate::plugin::exported_tables::ExportedTable; + pub use crate::plugin::exported_tables::FieldValue; + pub use crate::plugin::exported_tables::StaticField; + pub use crate::plugin::exported_tables::TableValues; pub use crate::plugin::tables::data::Bool; pub use crate::plugin::tables::data::TableData; pub use crate::plugin::tables::data::TypedTableField; @@ -449,6 +507,7 @@ pub mod tables { pub use crate::plugin::tables::entry::TableEntryReader; pub use crate::plugin::tables::table::TypedTable; pub use crate::plugin::tables::table_reader::TableReader; + pub use falco_event::fields::TypeId; } mod plugin; diff --git a/falco_plugin/src/plugin/exported_tables/mod.rs b/falco_plugin/src/plugin/exported_tables/mod.rs index c13ae08e..1a35edd5 100644 --- a/falco_plugin/src/plugin/exported_tables/mod.rs +++ b/falco_plugin/src/plugin/exported_tables/mod.rs @@ -7,10 +7,168 @@ use falco_event::fields::TypeId; use falco_plugin_api::{ss_plugin_state_data, ss_plugin_state_type, ss_plugin_table_fieldinfo}; use crate::plugin::tables::data::TableData; +use crate::tables::Bool; use crate::FailureReason; pub(super) mod wrappers; +mod seal { + pub trait Sealed {} +} + +/// Trait implemented for types that can be table fields (both static and containers for dynamic fields) +/// +/// This trait is sealed, meaning you cannot add new implementations (the list is limited +/// by the Falco plugin API) +pub trait FieldValue: seal::Sealed + Sized { + /// Store a C representation of `&self` in `out` + /// + /// This method must return `None` (and do nothing) if `&self` cannot be represented + /// as a value of type [`TypeId`]. + fn to_data(&self, out: &mut ss_plugin_state_data, type_id: TypeId) -> Option<()>; + + /// Load value from a C representation in `value` + /// + /// This method must return `None` (and do nothing) if `Self` cannot represent + /// a value of type [`TypeId`]. + /// + /// # Safety + /// `value` must be a valid reference with the union member described by [`TypeId`] filled + /// with valid data. + unsafe fn from_data(value: &ss_plugin_state_data, type_id: TypeId) -> Option; +} + +/// Trait implemented for types that can be static table fields +/// +/// This trait is sealed, meaning you cannot add new implementations (the list is limited +/// by the Falco plugin API) +pub trait StaticField: FieldValue { + /// The type id corresponding to the implementing type + const TYPE_ID: TypeId; +} + +macro_rules! impl_field_value { + ($ty:ty => $datafield:ident => $type_id:expr => $variant:ident) => { + impl seal::Sealed for $ty {} + + impl FieldValue for $ty { + fn to_data(&self, out: &mut ss_plugin_state_data, type_id: TypeId) -> Option<()> { + if type_id != $type_id { + return None; + } + + out.$datafield = *self; + Some(()) + } + + unsafe fn from_data(value: &ss_plugin_state_data, type_id: TypeId) -> Option { + if type_id != $type_id { + return None; + } + + Some(value.$datafield) + } + } + + impl StaticField for $ty { + const TYPE_ID: TypeId = $type_id; + } + + impl TryFrom for $ty { + type Error = FailureReason; + + fn try_from(value: DynamicFieldValue) -> Result { + if let DynamicFieldValue::$variant(val) = value { + Ok(val) + } else { + Err(FailureReason::Failure) + } + } + } + }; +} + +impl_field_value!(u8 => u8_ => TypeId::U8 => U8); +impl_field_value!(i8 => s8 => TypeId::I8 => I8); +impl_field_value!(u16 => u16_ => TypeId::U16 => U16); +impl_field_value!(i16 => s16 => TypeId::I16 => I16); +impl_field_value!(u32 => u32_ => TypeId::U32 => U32); +impl_field_value!(i32 => s32 => TypeId::I32 => I32); +impl_field_value!(u64 => u64_ => TypeId::U64 => U64); +impl_field_value!(i64 => s64 => TypeId::I64 => I64); + +impl seal::Sealed for bool {} +impl FieldValue for bool { + fn to_data(&self, out: &mut ss_plugin_state_data, type_id: TypeId) -> Option<()> { + if type_id != TypeId::Bool { + return None; + } + + out.b = if *self { 1 } else { 0 }; + Some(()) + } + + unsafe fn from_data(value: &ss_plugin_state_data, type_id: TypeId) -> Option { + if type_id != TypeId::Bool { + return None; + } + + Some(if value.b != 0 { true } else { false }) + } +} + +impl StaticField for bool { + const TYPE_ID: TypeId = TypeId::Bool; +} + +impl TryFrom for bool { + type Error = FailureReason; + + fn try_from(value: DynamicFieldValue) -> Result { + if let DynamicFieldValue::Bool(b) = value { + Ok(b) + } else { + Err(FailureReason::Failure) + } + } +} + +impl seal::Sealed for CString {} +impl FieldValue for CString { + fn to_data(&self, out: &mut ss_plugin_state_data, type_id: TypeId) -> Option<()> { + if type_id != TypeId::CharBuf { + return None; + } + + out.str_ = self.as_ptr(); + Some(()) + } + + unsafe fn from_data(value: &ss_plugin_state_data, type_id: TypeId) -> Option { + if type_id != TypeId::CharBuf { + return None; + } + + Some(CStr::from_ptr(value.str_).to_owned()) + } +} + +impl StaticField for CString { + const TYPE_ID: TypeId = TypeId::CharBuf; +} + +impl TryFrom for CString { + type Error = FailureReason; + + fn try_from(value: DynamicFieldValue) -> Result { + if let DynamicFieldValue::String(s) = value { + Ok(s) + } else { + Err(FailureReason::Failure) + } + } +} + /// # A value actually stored in a dynamic table /// /// This corresponds to `ss_plugin_state_data` in the plugin API. @@ -28,7 +186,8 @@ pub enum DynamicFieldValue { String(CString), } -impl DynamicFieldValue { +impl seal::Sealed for DynamicFieldValue {} +impl FieldValue for DynamicFieldValue { fn to_data(&self, out: &mut ss_plugin_state_data, type_id: TypeId) -> Option<()> { match self { DynamicFieldValue::U8(v) if type_id == TypeId::U8 => out.u8_ = *v, @@ -76,19 +235,88 @@ impl DynamicFieldValue { pub struct DynamicField { index: usize, type_id: TypeId, + read_only: bool, +} + +/// A table value type that only has dynamic fields +pub type DynamicFieldValues = BTreeMap; + +/// # A trait for structs that can be stored as table values +/// +/// For tables with dynamic fields only, it's easiest to use the [`DynamicFieldValues`] type +/// directly, for other types, you'll probably want to use the [`crate::TableValues`] derive macro. +pub trait TableValues: Default { + /// A list of all static fields in this table + const STATIC_FIELDS: &'static [(&'static CStr, TypeId, bool)]; + + /// True if this table supports adding custom fields, false otherwise + const HAS_DYNAMIC_FIELDS: bool; + + /// Get field value by index + /// + /// This method must verify that `type_id` is correct for the underlying data type + /// of the `key`th field and store the field's value in `out`. + /// + /// `key` will correspond to an entry in [`TableValues::STATIC_FIELDS`] or to a dynamic field + /// (if it's larger than `STATIC_FIELDS.size()`) + fn get( + &self, + key: usize, + type_id: TypeId, + out: &mut ss_plugin_state_data, + ) -> Result<(), FailureReason>; + + /// Set field value by index + /// + /// This method must verify that `type_id` is correct for the underlying data type + /// and store `value` under the (numeric) `key`. + /// + /// `key` will correspond to an entry in [`TableValues::STATIC_FIELDS`] or to a dynamic field + /// (if it's larger than `STATIC_FIELDS.size()`) + fn set(&mut self, key: usize, value: DynamicFieldValue) -> Result<(), FailureReason>; +} + +impl TableValues for DynamicFieldValues { + const STATIC_FIELDS: &'static [(&'static CStr, TypeId, bool)] = &[]; + const HAS_DYNAMIC_FIELDS: bool = true; + + fn get( + &self, + key: usize, + type_id: TypeId, + out: &mut ss_plugin_state_data, + ) -> Result<(), FailureReason> { + let Some((_, actual_type_id, _)) = Self::STATIC_FIELDS.get(key) else { + return Err(FailureReason::Failure); + }; + if type_id != *actual_type_id { + return Err(FailureReason::Failure); + } + + self.get(&key) + .and_then(|v| v.to_data(out, type_id)) + .ok_or(FailureReason::Failure) + } + + fn set(&mut self, key: usize, value: DynamicFieldValue) -> Result<(), FailureReason> { + self.insert(key, value); + Ok(()) + } } -// TODO(sdk) consider predefined fields (with a derive) // TODO(sdk) maybe use tinyvec (here, for storage and for extractions) -/// # A table with dynamic fields only +/// # A table with dynamic fields only by default /// /// An instance of this type can be exposed to other plugins via /// [`base::TableInitInput::add_table`](`crate::base::TableInitInput::add_table`) -pub struct DynamicTable { - name: CString, +/// +/// To create a table that includes static fields, pass a type that implements +/// [`TableValues`] as the second generic parameter. +pub struct DynamicTable { + name: &'static CStr, fields: BTreeMap>, field_descriptors: Vec, - data: BTreeMap>>>, + data: BTreeMap>>, } /// # A table that can be exported to other plugins @@ -107,7 +335,7 @@ pub trait ExportedTable { type Field; /// Return the table name. - fn name(&self) -> &CStr; + fn name(&self) -> &'static CStr; /// Return the number of entries in the table. fn size(&self) -> usize; @@ -161,16 +389,39 @@ pub trait ExportedTable { fn get_field(&self, name: &CStr, field_type: TypeId) -> Option>; /// Add a new field to the table - fn add_field(&mut self, name: &CStr, field_type: TypeId) -> Option>; + fn add_field( + &mut self, + name: &CStr, + field_type: TypeId, + read_only: bool, + ) -> Option>; +} + +impl DynamicTable { + /// Create a new table + pub fn new(name: &'static CStr) -> Self { + let mut table = Self { + name, + fields: Default::default(), + field_descriptors: vec![], + data: BTreeMap::new(), + }; + + for (name, field_type, read_only) in V::STATIC_FIELDS { + table.add_field(name, *field_type, *read_only); + } + + table + } } -impl ExportedTable for DynamicTable { +impl ExportedTable for DynamicTable { type Key = K; - type Entry = RefCell>; + type Entry = RefCell; type Field = DynamicField; - fn name(&self) -> &CStr { - self.name.as_c_str() + fn name(&self) -> &'static CStr { + self.name } fn size(&self) -> usize { @@ -189,11 +440,7 @@ impl ExportedTable for DynamicTable { ) -> Result<(), FailureReason> { let (type_id, index) = { (field.type_id, field.index) }; - entry - .borrow() - .get(&index) - .and_then(|val| val.to_data(out, type_id)) - .ok_or(FailureReason::Failure) + entry.borrow().get(index, type_id, out) } fn iterate_entries(&mut self, mut func: F) -> bool @@ -218,7 +465,7 @@ impl ExportedTable for DynamicTable { } fn create_entry() -> Rc { - Rc::new(RefCell::new(BTreeMap::new())) + Rc::new(RefCell::new(V::default())) } fn add(&mut self, key: &Self::Key, entry: Rc) -> Option> { @@ -233,14 +480,17 @@ impl ExportedTable for DynamicTable { field: &Rc, value: &ss_plugin_state_data, ) -> Result<(), FailureReason> { + if field.read_only { + return Err(FailureReason::NotSupported); + } + let (type_id, index) = { (field.type_id, field.index) }; let value = unsafe { DynamicFieldValue::from_data(value, type_id).ok_or(FailureReason::Failure)? }; let mut entry = entry.borrow_mut(); - entry.insert(index, value); - Ok(()) + entry.set(index, value) } fn list_fields(&mut self) -> &[ss_plugin_table_fieldinfo] { @@ -255,8 +505,20 @@ impl ExportedTable for DynamicTable { Some(Rc::clone(field)) } - fn add_field(&mut self, name: &CStr, field_type: TypeId) -> Option> { - if self.fields.get(name).is_some() { + fn add_field( + &mut self, + name: &CStr, + field_type: TypeId, + read_only: bool, + ) -> Option> { + if let Some(existing_field) = self.fields.get(name) { + if existing_field.type_id == field_type && existing_field.read_only == read_only { + return Some(Rc::clone(existing_field)); + } + return None; + } + + if !V::HAS_DYNAMIC_FIELDS { return None; } @@ -266,13 +528,14 @@ impl ExportedTable for DynamicTable { let field = Rc::new(DynamicField { index, type_id: field_type, + read_only, }); self.fields.insert(name.clone(), Rc::clone(&field)); self.field_descriptors.push(ss_plugin_table_fieldinfo { name: name.into_raw(), field_type: field_type as ss_plugin_state_type, - read_only: 0, // TODO(sdk) support read-only fields + read_only: Bool::from(read_only).0, }); Some(field) diff --git a/falco_plugin/src/plugin/exported_tables/wrappers.rs b/falco_plugin/src/plugin/exported_tables/wrappers.rs index d0b8bd39..8faccf37 100644 --- a/falco_plugin/src/plugin/exported_tables/wrappers.rs +++ b/falco_plugin/src/plugin/exported_tables/wrappers.rs @@ -266,7 +266,7 @@ unsafe extern "C" fn add_table_field( } else { CStr::from_ptr(name) }; - match table.add_field(name, data_type) { + match table.add_field(name, data_type, false) { Some(field) => Box::into_raw(Box::new(field)) as *mut _, None => std::ptr::null_mut(), } diff --git a/falco_plugin/src/plugin/tables/data.rs b/falco_plugin/src/plugin/tables/data.rs index a7bde244..3adf58fe 100644 --- a/falco_plugin/src/plugin/tables/data.rs +++ b/falco_plugin/src/plugin/tables/data.rs @@ -66,7 +66,7 @@ impl_table_data_for_numeric_type!(i64 => s64: TypeId::I64); /// /// This type serves as a wrapper, exposing conversion methods to/from Rust bool. #[repr(transparent)] -pub struct Bool(ss_plugin_bool); +pub struct Bool(pub(crate) ss_plugin_bool); impl From for Bool { fn from(value: bool) -> Self { diff --git a/falco_plugin/src/plugin/tables/ffi.rs b/falco_plugin/src/plugin/tables/ffi.rs index 55607636..3ba3cdec 100644 --- a/falco_plugin/src/plugin/tables/ffi.rs +++ b/falco_plugin/src/plugin/tables/ffi.rs @@ -1,14 +1,12 @@ use std::ffi::CStr; -use anyhow::Error; - use falco_plugin_api::{ ss_plugin_init_input, ss_plugin_state_type, ss_plugin_table_fields_vtable, ss_plugin_table_info, ss_plugin_table_input, ss_plugin_table_reader_vtable, ss_plugin_table_writer_vtable, }; -use crate::plugin::error::{AsResult, LastError}; +use crate::plugin::error::AsResult; use crate::plugin::exported_tables::wrappers::{fields_vtable, reader_vtable, writer_vtable}; use crate::plugin::exported_tables::ExportedTable; use crate::plugin::tables::data::TableData; @@ -105,9 +103,8 @@ pub trait InitInput { /// introducing a custom derive macro for the ExportedTable trait. fn add_table>( &self, - name: &'static CStr, - table: Box, - ) -> Result<(), Error>; + table: T, + ) -> Result<&'static mut T, FailureReason>; } impl InitInput for ss_plugin_init_input { @@ -145,26 +142,25 @@ impl InitInput for ss_plugin_init_input { fn add_table( &self, - name: &'static CStr, - table: Box, - ) -> Result<(), Error> { + table: T, + ) -> Result<&'static mut T, FailureReason> { let vtable = unsafe { self.tables.as_ref() }.ok_or(FailureReason::Failure)?; let add_table = vtable.add_table.ok_or(FailureReason::Failure)?; - // Safety: we pass the data directly from FFI, the framework would never lie to us, right? - let last_err = unsafe { LastError::new(self.owner, self.get_owner_last_error) }; - let mut reader_vtable_ext = reader_vtable::(); let mut writer_vtable_ext = writer_vtable::(); let mut fields_vtable_ext = fields_vtable::(); + let mut table = Box::new(table); + let table_ptr = table.as_mut() as *mut T; + // Note: we lend the ss_plugin_table_input to the FFI api and do not need // to hold on to it (everything is copied out), but the name field is copied // as a pointer, so the name we receive must be a 'static ref let table_input = ss_plugin_table_input { - name: name.as_ptr(), + name: table.name().as_ptr(), key_type: K::TYPE_ID as ss_plugin_state_type, - table: Box::into_raw(table) as *mut _, + table: table_ptr.cast(), reader: ss_plugin_table_reader_vtable { get_table_name: reader_vtable_ext.get_table_name, get_table_size: reader_vtable_ext.get_table_size, @@ -189,7 +185,9 @@ impl InitInput for ss_plugin_init_input { fields_ext: &mut fields_vtable_ext as *mut _, }; - unsafe { add_table(self.owner, &table_input as *const _) } - .as_result_with_last_error(&last_err) + unsafe { add_table(self.owner, &table_input as *const _) }.as_result()?; + // There is no API for destroying a table, so we leak the pointer. At least we can have + // a &'static back + Ok(Box::leak(table)) } } diff --git a/falco_plugin_derive/Cargo.toml b/falco_plugin_derive/Cargo.toml new file mode 100644 index 00000000..565325e2 --- /dev/null +++ b/falco_plugin_derive/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "falco_plugin_derive" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +description = "Derive macros for the falco_plugin library" +homepage = "https://gnosek.github.io/falco-plugin-rs/falco_plugin_derive/" +repository = "https://github.com/gnosek/falco-plugin-rs" +readme = "README.md" +keywords = ["falco", "security"] + +[lib] +proc-macro = true + +[dependencies] +quote = "1" +proc-macro2 = "1.0" +syn = "2" \ No newline at end of file diff --git a/falco_plugin_derive/README.md b/falco_plugin_derive/README.md new file mode 100644 index 00000000..e61da09a --- /dev/null +++ b/falco_plugin_derive/README.md @@ -0,0 +1,5 @@ +# Derive macros for `falco_plugin` + +This crate currently contains the derive macro for the [`falco_plugin::tables::TableValues`] trait. It is exported +as [`falco_plugin::TableValues`] and is documented there (since the generated code depends on `falco_plugin`, +adding example code to this crate would introduce a circular dependency). \ No newline at end of file diff --git a/falco_plugin_derive/src/lib.rs b/falco_plugin_derive/src/lib.rs new file mode 100644 index 00000000..ae1a3aec --- /dev/null +++ b/falco_plugin_derive/src/lib.rs @@ -0,0 +1,137 @@ +#![doc = include_str!("../README.md")] +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(TableValues, attributes(static_only, dynamic, readonly, hidden))] +pub fn derive_table_values(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let static_only = syn::Ident::new("static_only", input.span()); + let hidden = syn::Ident::new("hidden", input.span()); + let readonly = syn::Ident::new("readonly", input.span()); + let dynamic = syn::Ident::new("dynamic", input.span()); + + let static_only = input + .attrs + .iter() + .any(|a| a.meta.path().is_ident(&static_only)); + + let syn::Data::Struct(data) = input.data else { + return TokenStream::from( + syn::Error::new( + input.ident.span(), + "Only structs with named fields can derive `TableValues`", + ) + .to_compile_error(), + ); + }; + + let name = &input.ident; + let syn::Fields::Named(fields) = data.fields else { + return TokenStream::from( + syn::Error::new( + input.ident.span(), + "Only structs with named fields can derive `TableValues`", + ) + .to_compile_error(), + ); + }; + + let fields = fields.named; + + let dynamic_fields = fields + .iter() + .filter(|f| f.attrs.iter().any(|a| a.meta.path().is_ident(&dynamic))) + .collect::>(); + + let (dynamic_field_get, dynamic_field_set) = match (static_only, dynamic_fields.len()) { + (false, 0) | (true, 1) => { + return TokenStream::from( + syn::Error::new( + name.span(), + "Struct must have exactly one #[dynamic] field or be marked as #[static_only]", + ) + .to_compile_error(), + ); + } + (true, 0) => ( + quote!(Err(::falco_plugin::FailureReason::NotSupported)), + quote!(Err(::falco_plugin::FailureReason::NotSupported)), + ), + (false, 1) => { + let dynamic_field_name = dynamic_fields[0].ident.as_ref().unwrap(); + ( + quote!(::falco_plugin::tables::TableValues::get(&self.#dynamic_field_name, key, type_id, out)), + quote!(self.#dynamic_field_name.set(key, value)), + ) + } + _ => { + return TokenStream::from( + syn::Error::new( + dynamic_fields[1].span(), + "Struct must have exactly one #[dynamic] field or be marked as #[static_only]", + ) + .to_compile_error(), + ); + } + }; + + let visible_static_fields = fields.iter().filter(|f| { + !f.attrs + .iter() + .any(|a| a.meta.path().is_ident(&hidden) || a.meta.path().is_ident(&dynamic)) + }); + + let static_fields = visible_static_fields.clone().map(|f| { + let readonly = f.attrs.iter().any(|a| a.meta.path().is_ident(&readonly)); + let name = f.ident.as_ref().unwrap().to_string(); + let ty = &f.ty; + + quote!( (::falco_plugin::c!(#name), <#ty as ::falco_plugin::tables::StaticField>::TYPE_ID, #readonly) ) + }); + + let static_field_gets = visible_static_fields.clone().enumerate().map(|(i, f)| { + let name = f.ident.as_ref().unwrap(); + quote!(#i => self.#name.to_data(out, type_id).ok_or(::falco_plugin::FailureReason::Failure)) + }); + + let static_field_sets = visible_static_fields.clone().enumerate().map(|(i, f)| { + let name = f.ident.as_ref().unwrap(); + quote!(#i => Ok(self.#name = value.try_into()?)) + }); + + let has_dynamic_fields = !static_only; + quote!( + impl ::falco_plugin::tables::TableValues for #name { + const STATIC_FIELDS: &'static [(&'static ::std::ffi::CStr, ::falco_plugin::tables::TypeId, bool)] = &[ + #(#static_fields,)* + ]; + const HAS_DYNAMIC_FIELDS: bool = #has_dynamic_fields; + + fn get( + &self, + key: usize, + type_id: ::falco_plugin::tables::TypeId, + out: &mut ::falco_plugin::api::ss_plugin_state_data, + ) -> Result<(), ::falco_plugin::FailureReason> { + use ::falco_plugin::tables::TableValues; + use ::falco_plugin::tables::FieldValue; + match key { + #(#static_field_gets,)* + _ => #dynamic_field_get, + } + } + + fn set(&mut self, key: usize, value: ::falco_plugin::tables::DynamicFieldValue) -> Result<(), ::falco_plugin::FailureReason> { + use ::falco_plugin::tables::TableValues; + use ::falco_plugin::tables::FieldValue; + match key { + #(#static_field_sets,)* + _ => #dynamic_field_set, + } + } + } + ) + .into() +}