diff --git a/falco_plugin/src/tables/import/field_info.rs b/falco_plugin/src/tables/import/field_info.rs new file mode 100644 index 00000000..a5a761c6 --- /dev/null +++ b/falco_plugin/src/tables/import/field_info.rs @@ -0,0 +1,44 @@ +use crate::tables::FieldTypeId; +use num_traits::FromPrimitive; +use std::fmt::Debug; + +/// Information about a table field +#[repr(transparent)] +pub struct FieldInfo(falco_plugin_api::ss_plugin_table_fieldinfo); + +impl Debug for FieldInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FieldInfo") + .field("name", &self.name()) + .field("read_only", &self.read_only()) + .field("field_type", &self.field_type()) + .finish() + } +} + +impl FieldInfo { + /// Returns the name of the field + /// + /// If the field name cannot be represented as UTF-8, returns "<invalid field name>" + pub fn name(&self) -> &str { + unsafe { std::ffi::CStr::from_ptr(self.0.name) } + .to_str() + .unwrap_or("") + } + + /// Returns true if the current field is read-only + pub fn read_only(&self) -> bool { + self.0.read_only != 0 + } + + /// Returns the type of the field + /// + /// If the type cannot be represented as a [`FieldTypeId`], returns an error + /// with the raw field type value. + pub fn field_type(&self) -> Result { + match FieldTypeId::from_u32(self.0.field_type) { + Some(field_type) => Ok(field_type), + None => Err(self.0.field_type), + } + } +} diff --git a/falco_plugin/src/tables/import/mod.rs b/falco_plugin/src/tables/import/mod.rs index 860e380f..c4d3019e 100644 --- a/falco_plugin/src/tables/import/mod.rs +++ b/falco_plugin/src/tables/import/mod.rs @@ -323,10 +323,12 @@ mod entry; mod field; +mod field_info; mod macros; mod runtime; mod runtime_table_validator; mod table; +mod table_info; mod table_input; // for macro use only @@ -337,8 +339,10 @@ pub use crate::tables::data::Bool; pub use crate::tables::data::TableData; pub use entry::Entry; pub use field::Field; +pub use field_info::FieldInfo; pub use runtime::RuntimeEntry; pub use table::Table; +pub use table_info::TableInfo; // for macro use only #[doc(hidden)] diff --git a/falco_plugin/src/tables/import/table/mod.rs b/falco_plugin/src/tables/import/table/mod.rs index d7100434..b24cea72 100644 --- a/falco_plugin/src/tables/import/table/mod.rs +++ b/falco_plugin/src/tables/import/table/mod.rs @@ -1,16 +1,15 @@ use crate::tables::data::{seal, FieldTypeId, Key, TableData, Value}; -use crate::tables::import::entry; use crate::tables::import::field::Field; use crate::tables::import::runtime::NoMetadata; use crate::tables::import::runtime_table_validator::RuntimeTableValidator; use crate::tables::import::table::raw::{IterationResult, RawTable}; use crate::tables::import::traits::{Entry, TableAccess, TableMetadata}; -use crate::tables::TableFields; +use crate::tables::import::{entry, FieldInfo}; use crate::tables::TableReader; use crate::tables::TableWriter; use crate::tables::TablesInput; use anyhow::Error; -use falco_plugin_api::{ss_plugin_state_data, ss_plugin_table_field_t, ss_plugin_table_fieldinfo}; +use falco_plugin_api::{ss_plugin_state_data, ss_plugin_table_field_t}; use std::ffi::CStr; use std::marker::PhantomData; use std::ops::ControlFlow; @@ -145,12 +144,8 @@ where } /// # List the available fields - /// - /// **Note**: this method is of limited utility in actual plugin code (you know the fields you - /// want to access), so it returns the unmodified structure from the plugin API, including - /// raw pointers to C-style strings. This may change later. - pub fn list_fields(&self, fields_vtable: &TableFields) -> &[ss_plugin_table_fieldinfo] { - self.raw_table.list_fields(fields_vtable) + pub fn list_fields(&self, tables_input: &TablesInput) -> &[FieldInfo] { + self.raw_table.list_fields(tables_input) } /// # Get a table field by name diff --git a/falco_plugin/src/tables/import/table/raw.rs b/falco_plugin/src/tables/import/table/raw.rs index 0a86e024..ff4714b7 100644 --- a/falco_plugin/src/tables/import/table/raw.rs +++ b/falco_plugin/src/tables/import/table/raw.rs @@ -4,7 +4,7 @@ use crate::tables::data::{FieldTypeId, Key, Value}; use crate::tables::import::entry::raw::RawEntry; use crate::tables::import::field::raw::RawField; use crate::tables::import::traits::TableMetadata; -use crate::tables::TableFields; +use crate::tables::import::FieldInfo; use crate::tables::TableReader; use crate::tables::TableReaderImpl; use crate::tables::TableWriter; @@ -12,8 +12,8 @@ use crate::tables::TableWriterImpl; use crate::tables::TablesInput; use falco_plugin_api::{ ss_plugin_bool, ss_plugin_rc_SS_PLUGIN_SUCCESS, ss_plugin_state_data, ss_plugin_state_type, - ss_plugin_table_entry_t, ss_plugin_table_field_t, ss_plugin_table_fieldinfo, - ss_plugin_table_iterator_func_t, ss_plugin_table_iterator_state_t, ss_plugin_table_t, + ss_plugin_table_entry_t, ss_plugin_table_field_t, ss_plugin_table_iterator_func_t, + ss_plugin_table_iterator_state_t, ss_plugin_table_t, }; use num_traits::FromPrimitive; use std::ffi::CStr; @@ -65,19 +65,22 @@ pub struct RawTable { impl RawTable { /// # List the available fields - /// - /// **Note**: this method is of limited utility in actual plugin code (you know the fields you - /// want to access), so it returns the unmodified structure from the plugin API, including - /// raw pointers to C-style strings. This may change later. - pub fn list_fields(&self, fields_vtable: &TableFields) -> &[ss_plugin_table_fieldinfo] { + pub fn list_fields(&self, tables_input: &TablesInput) -> &[FieldInfo] { let mut num_fields = 0u32; - let fields = fields_vtable + let fields = tables_input + .fields_ext .list_table_fields(self.table, &mut num_fields as *mut _) - .unwrap_or(std::ptr::null_mut()); + .unwrap_or(std::ptr::null()); if fields.is_null() { &[] } else { - unsafe { std::slice::from_raw_parts(fields, num_fields as usize) } + // SAFETY: the plugin API guarantees that `fields` is valid and points + // to an array of `num_fields` `ss_plugin_table_fieldinfo` elements. + // `FieldInfo` is a `#[repr(transparent)]` wrapper around + // `ss_plugin_table_fieldinfo`, so casting the pointer to `FieldInfo` + // preserves the layout, and `from_raw_parts` can build a slice over + // those `num_fields` elements. + unsafe { std::slice::from_raw_parts(fields.cast(), num_fields as usize) } } } diff --git a/falco_plugin/src/tables/import/table_info.rs b/falco_plugin/src/tables/import/table_info.rs new file mode 100644 index 00000000..f0ff6153 --- /dev/null +++ b/falco_plugin/src/tables/import/table_info.rs @@ -0,0 +1,38 @@ +use crate::tables::FieldTypeId; +use num_traits::FromPrimitive; +use std::fmt::Debug; + +/// Information about a table +#[repr(transparent)] +pub struct TableInfo(falco_plugin_api::ss_plugin_table_info); + +impl Debug for TableInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TableInfo") + .field("name", &self.name()) + .field("key_type", &self.key_type()) + .finish() + } +} + +impl TableInfo { + /// Returns the name of the table + /// + /// If the table name cannot be represented as UTF-8, returns "<invalid table name>" + pub fn name(&self) -> &str { + unsafe { std::ffi::CStr::from_ptr(self.0.name) } + .to_str() + .unwrap_or("") + } + + /// Returns the type of the table's key + /// + /// If the type cannot be represented as a [`FieldTypeId`], returns an error + /// with the raw field type value. + pub fn key_type(&self) -> Result { + match FieldTypeId::from_u32(self.0.key_type) { + Some(field_type) => Ok(field_type), + None => Err(self.0.key_type), + } + } +} diff --git a/falco_plugin/src/tables/import/table_input.rs b/falco_plugin/src/tables/import/table_input.rs index 22310b82..33b226eb 100644 --- a/falco_plugin/src/tables/import/table_input.rs +++ b/falco_plugin/src/tables/import/table_input.rs @@ -1,6 +1,6 @@ use crate::error::as_result::WithLastError; use crate::tables::import::traits::{TableAccess, TableMetadata}; -use crate::tables::import::RawTable; +use crate::tables::import::{RawTable, TableInfo}; use crate::tables::{Key, TablesInput}; use falco_plugin_api::ss_plugin_state_type; use std::ffi::CStr; @@ -31,4 +31,15 @@ impl TablesInput<'_> { Ok(T::new(table, metadata, false)) } } + + /// # List the available tables + pub fn list_tables(&self) -> &[TableInfo] { + let mut num_tables = 0u32; + let tables = unsafe { (self.list_tables)(self.owner, &mut num_tables as *mut _) }; + if tables.is_null() { + &[] + } else { + unsafe { std::slice::from_raw_parts(tables.cast(), num_tables as usize) } + } + } } diff --git a/falco_plugin/src/tables/mod.rs b/falco_plugin/src/tables/mod.rs index 03c6c32c..a23f0b2b 100644 --- a/falco_plugin/src/tables/mod.rs +++ b/falco_plugin/src/tables/mod.rs @@ -138,7 +138,7 @@ //! can use them from your plugin (e.g. in a separate thread) concurrently to other plugins //! (in the main thread). -pub(crate) use vtable::fields::TableFields; +pub use crate::tables::data::FieldTypeId; pub(crate) use vtable::reader::private::TableReaderImpl; pub use vtable::reader::LazyTableReader; pub use vtable::reader::TableReader; @@ -157,7 +157,3 @@ mod vtable; // for macro use only #[doc(hidden)] pub use crate::tables::data::{Key, Value}; - -// for macro use only -#[doc(hidden)] -pub use crate::tables::data::FieldTypeId; diff --git a/falco_plugin/src/tables/vtable/mod.rs b/falco_plugin/src/tables/vtable/mod.rs index eaba76c8..ca521338 100644 --- a/falco_plugin/src/tables/vtable/mod.rs +++ b/falco_plugin/src/tables/vtable/mod.rs @@ -99,20 +99,3 @@ impl TablesInput<'_> { } } } - -impl TablesInput<'_> { - /// # List the available tables - /// - /// **Note**: this method is of limited utility in actual plugin code (you know the tables you - /// want to access), so it returns the unmodified structure from the plugin API, including - /// raw pointers to C-style strings. This may change later. - pub fn list_tables(&self) -> &[ss_plugin_table_info] { - let mut num_tables = 0u32; - let tables = unsafe { (self.list_tables)(self.owner, &mut num_tables as *mut _) }; - if tables.is_null() { - &[] - } else { - unsafe { std::slice::from_raw_parts(tables, num_tables as usize) } - } - } -} diff --git a/falco_plugin_tests/tests/list_fields.rs b/falco_plugin_tests/tests/list_fields.rs new file mode 100644 index 00000000..64428703 --- /dev/null +++ b/falco_plugin_tests/tests/list_fields.rs @@ -0,0 +1,88 @@ +use anyhow::Error; +use falco_plugin::base::Plugin; +use falco_plugin::event::events::Event; +use falco_plugin::event::{EventInput, PluginEvent}; +use falco_plugin::parse::{ParseInput, ParsePlugin}; +use falco_plugin::static_plugin; +use falco_plugin::tables::{FieldTypeId, TablesInput}; +use falco_plugin_tests::plugin_collection::events::countdown::Countdown; +use falco_plugin_tests::plugin_collection::parse::remaining_into_table_direct::PARSE_REMAINING_INTO_TABLE_DIRECT_PLUGIN_API; +use falco_plugin_tests::plugin_collection::source::countdown::COUNTDOWN_PLUGIN_API; +use falco_plugin_tests::plugin_collection::tables::remaining_import::RemainingCounterImportTable; +use falco_plugin_tests::{init_plugin, instantiate_tests, TestDriver}; +use std::ffi::CStr; + +struct ListFieldsPlugin { + #[allow(unused)] + remaining_table_import: RemainingCounterImportTable, +} + +impl Plugin for ListFieldsPlugin { + const NAME: &'static CStr = c"dummy"; + const PLUGIN_VERSION: &'static CStr = c"0.0.0"; + const DESCRIPTION: &'static CStr = c"test plugin"; + const CONTACT: &'static CStr = c"rust@localdomain.pl"; + type ConfigType = (); + + fn new(input: Option<&TablesInput>, _config: Self::ConfigType) -> Result { + let input = input.ok_or_else(|| anyhow::anyhow!("did not get table input"))?; + + let remaining_table_import: RemainingCounterImportTable = input.get_table(c"remaining")?; + + let fields = remaining_table_import.list_fields(input); + let mut num_fields = 0; + for field in fields { + match field.name() { + "remaining" => { + anyhow::ensure!(field.read_only() == false); + anyhow::ensure!(field.field_type() == Ok(FieldTypeId::U64)); + num_fields += 1; + } + "readonly" => { + anyhow::ensure!(field.read_only() == true); + anyhow::ensure!(field.field_type() == Ok(FieldTypeId::U64)); + num_fields += 1; + } + "countdown" => { + anyhow::ensure!(field.read_only() == true); + anyhow::ensure!(field.field_type() == Ok(FieldTypeId::Table)); + num_fields += 1; + } + name => panic!("unknown field: {name}"), + } + } + anyhow::ensure!(num_fields == 3); + + Ok(Self { + remaining_table_import, + }) + } +} + +impl ParsePlugin for ListFieldsPlugin { + type Event<'a> = Event>>; + + fn parse_event( + &mut self, + _event: &EventInput>, + _parse_input: &ParseInput, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +static_plugin!(pub LIST_FIELDS_API = ListFieldsPlugin); + +fn test_list_fields() { + let (mut driver, _plugin) = init_plugin::( + &COUNTDOWN_PLUGIN_API, + cr#"{"remaining": 4, "batch_size": 4}"#, + ) + .unwrap(); + driver + .register_plugin(&PARSE_REMAINING_INTO_TABLE_DIRECT_PLUGIN_API, c"") + .unwrap(); + driver.register_plugin(&LIST_FIELDS_API, c"").unwrap(); +} + +instantiate_tests!(test_list_fields); diff --git a/falco_plugin_tests/tests/list_tables.rs b/falco_plugin_tests/tests/list_tables.rs new file mode 100644 index 00000000..4b471a0c --- /dev/null +++ b/falco_plugin_tests/tests/list_tables.rs @@ -0,0 +1,74 @@ +use anyhow::Error; +use falco_plugin::base::Plugin; +use falco_plugin::event::events::Event; +use falco_plugin::event::{EventInput, PluginEvent}; +use falco_plugin::parse::{ParseInput, ParsePlugin}; +use falco_plugin::static_plugin; +use falco_plugin::tables::{FieldTypeId, TablesInput}; +use falco_plugin_tests::plugin_collection::events::countdown::Countdown; +use falco_plugin_tests::plugin_collection::parse::remaining_into_table_direct::PARSE_REMAINING_INTO_TABLE_DIRECT_PLUGIN_API; +use falco_plugin_tests::plugin_collection::source::countdown::COUNTDOWN_PLUGIN_API; +use falco_plugin_tests::plugin_collection::tables::remaining_import::RemainingCounterImportTable; +use falco_plugin_tests::{init_plugin, instantiate_tests, TestDriver}; +use std::ffi::CStr; + +struct ListTablesPlugin { + #[allow(unused)] + remaining_table_import: RemainingCounterImportTable, +} + +impl Plugin for ListTablesPlugin { + const NAME: &'static CStr = c"dummy"; + const PLUGIN_VERSION: &'static CStr = c"0.0.0"; + const DESCRIPTION: &'static CStr = c"test plugin"; + const CONTACT: &'static CStr = c"rust@localdomain.pl"; + type ConfigType = (); + + fn new(input: Option<&TablesInput>, _config: Self::ConfigType) -> Result { + let input = input.ok_or_else(|| anyhow::anyhow!("did not get table input"))?; + + let tables = input.list_tables(); + let mut found_remaining = false; + for table in tables { + if table.name() == "remaining" { + found_remaining = true; + anyhow::ensure!(table.key_type() == Ok(FieldTypeId::U64)); + } + } + anyhow::ensure!(found_remaining); + + let remaining_table_import: RemainingCounterImportTable = input.get_table(c"remaining")?; + + Ok(Self { + remaining_table_import, + }) + } +} + +impl ParsePlugin for ListTablesPlugin { + type Event<'a> = Event>>; + + fn parse_event( + &mut self, + _event: &EventInput>, + _parse_input: &ParseInput, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +static_plugin!(pub LIST_TABLES_API = ListTablesPlugin); + +fn test_list_tables() { + let (mut driver, _plugin) = init_plugin::( + &COUNTDOWN_PLUGIN_API, + cr#"{"remaining": 4, "batch_size": 4}"#, + ) + .unwrap(); + driver + .register_plugin(&PARSE_REMAINING_INTO_TABLE_DIRECT_PLUGIN_API, c"") + .unwrap(); + driver.register_plugin(&LIST_TABLES_API, c"").unwrap(); +} + +instantiate_tests!(test_list_tables);