hexavalent/
ffi.rs

1use std::ffi::CStr;
2use std::marker::PhantomData;
3use std::os::raw::{c_char, c_int};
4use std::ptr::NonNull;
5
6use time::OffsetDateTime;
7
8use crate::str::HexStr;
9
10#[allow(missing_debug_implementations, missing_docs, unreachable_pub)]
11mod binding;
12
13mod handle;
14
15// constants https://hexchat.readthedocs.io/en/latest/plugins.html#types-and-constants
16pub(crate) use binding::{
17    HEXCHAT_EAT_ALL, HEXCHAT_EAT_HEXCHAT, HEXCHAT_EAT_NONE, HEXCHAT_EAT_PLUGIN, HEXCHAT_PRI_HIGH,
18    HEXCHAT_PRI_HIGHEST, HEXCHAT_PRI_LOW, HEXCHAT_PRI_LOWEST, HEXCHAT_PRI_NORM,
19};
20
21// types https://hexchat.readthedocs.io/en/latest/plugins.html#types-and-constants
22pub(crate) use binding::{hexchat_context, hexchat_event_attrs, hexchat_hook, hexchat_list};
23// this is used publicly by generated code
24pub use binding::hexchat_plugin;
25
26pub(crate) use handle::RawPluginHandle;
27
28// https://hexchat.readthedocs.io/en/latest/plugins.html#c.hexchat_emit_print
29const SUCCESS: c_int = 1;
30const FAILURE: c_int = 0;
31
32pub(crate) fn int_to_result(ret_code: c_int) -> Result<(), ()> {
33    match ret_code {
34        SUCCESS => Ok(()),
35        _ => Err(()),
36    }
37}
38
39pub(crate) fn result_to_int(res: Result<(), ()>) -> c_int {
40    match res {
41        Ok(()) => SUCCESS,
42        Err(_) => FAILURE,
43    }
44}
45
46/// Converts `word` or `word_eol` to an iterator over `&HexStr`.
47///
48/// # Safety
49///
50/// `word` must be a `word` or `word_eol` pointer from HexChat.
51///
52/// `word` must be valid for the entire lifetime `'a`.
53#[allow(clippy::trivially_copy_pass_by_ref)]
54pub(crate) unsafe fn word_to_iter<'a>(
55    word: &'a *mut *mut c_char,
56) -> impl Iterator<Item = &'a HexStr> {
57    // make it obvious that this is a non-raw-pointer deref
58    let word: *mut *mut c_char = *word;
59
60    // https://hexchat.readthedocs.io/en/latest/plugins.html#what-s-word-and-word-eol
61    // Safety: first index is reserved, per documentation
62    let word = unsafe { word.add(1) };
63
64    struct WordIter<'a> {
65        word: *mut *mut c_char,
66        _lifetime: PhantomData<&'a *mut c_char>,
67    }
68
69    impl<'a> Iterator for WordIter<'a> {
70        type Item = &'a HexStr;
71
72        fn next(&mut self) -> Option<Self::Item> {
73            // Safety: word points to a valid null-terminated array, so we cannot read past the end or wrap
74            let elem = unsafe { *self.word };
75            if elem.is_null() {
76                None
77            } else {
78                // Safety: elem is not null, so there is at least one more element in the array (possibly null)
79                self.word = unsafe { self.word.add(1) };
80                // Safety: word points to valid strings; words does not outlive 'a
81                let str = unsafe { CStr::from_ptr::<'a>(elem) };
82
83                let str = HexStr::from_cstr(str)
84                    .unwrap_or_else(|e| panic!("Invalid UTF8 in {:?}: {}", str, e));
85
86                Some(str)
87            }
88        }
89
90        fn nth(&mut self, mut n: usize) -> Option<Self::Item> {
91            while n > 0 {
92                let elem = unsafe { *self.word };
93                if elem.is_null() {
94                    break;
95                } else {
96                    // Safety: elem is not null, so there is at least one more element in the array (possibly null)
97                    self.word = unsafe { self.word.add(1) };
98                }
99                n -= 1;
100            }
101
102            self.next()
103        }
104    }
105
106    WordIter::<'a> {
107        word,
108        _lifetime: PhantomData,
109    }
110}
111
112#[allow(unreachable_pub)]
113#[derive(Debug)]
114pub struct ListElem<'a> {
115    raw: RawPluginHandle<'a>,
116    /// Always points to a valid list element.
117    list_ptr: NonNull<hexchat_list>,
118}
119
120impl<'a> ListElem<'a> {
121    /// Creates a safe wrapper around a list element.
122    ///
123    /// # Safety
124    ///
125    /// `list` must point to a `hexchat_list` element (e.g. one for which `hexchat_list_next` returned true),
126    /// which is valid for the entire lifetime `'a`.
127    ///
128    /// You must not interact with HexChat in any way that could invalidate this list elem while it exists.
129    /// Notably, this includes calling `hexchat_list_next` on the same list to get another element,
130    /// but may also include other operations (e.g. switching channels). To be safe, do not call
131    /// any HexChat functions while a `ListElem` exists.
132    pub(crate) unsafe fn new(raw: RawPluginHandle<'a>, list_ptr: NonNull<hexchat_list>) -> Self {
133        Self { raw, list_ptr }
134    }
135
136    pub(crate) fn string<'elem>(&'elem self, name: &CStr) -> Option<&'elem HexStr> {
137        // Safety: list_ptr is valid per ListElem precondition, name is a null-terminated string
138        let ptr = unsafe {
139            self.raw
140                .hexchat_list_str(self.list_ptr.as_ptr(), name.as_ptr())
141        };
142
143        if ptr.is_null() {
144            return None;
145        }
146
147        // Safety: hexchat_list_str gets a valid string or null, temporary does not outlive the list elem
148        let str = unsafe { CStr::from_ptr(ptr) };
149
150        let str = HexStr::from_cstr(str)
151            .unwrap_or_else(|e| panic!("Invalid UTF8 from `hexchat_list_str` in {:?}: {}", str, e));
152
153        Some(str)
154    }
155
156    pub(crate) fn int(&self, name: &CStr) -> i32 {
157        // Safety: list_ptr is valid per ListElem precondition, name is a null-terminated string
158        unsafe {
159            self.raw
160                .hexchat_list_int(self.list_ptr.as_ptr(), name.as_ptr())
161        }
162    }
163
164    pub(crate) fn time(&self, name: &CStr) -> OffsetDateTime {
165        // Safety: list_ptr is valid per ListElem precondition, name is a null-terminated string
166        let time = unsafe {
167            self.raw
168                .hexchat_list_time(self.list_ptr.as_ptr(), name.as_ptr())
169        };
170
171        OffsetDateTime::from_unix_timestamp(time)
172            .unwrap_or_else(|e| panic!("Invalid timestamp from `hexchat_list_time`: {}", e))
173    }
174}