hexchat_unsafe_plugin/lib.rs
1// Hexchat Plugin API Bindings for Rust
2// Copyright (C) 2018, 2021, 2022 Soni L.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as
6// published by the Free Software Foundation, either version 3 of the
7// License, or (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Write hexchat plugins in Rust!
18//!
19//! This library provides safe API bindings for hexchat, but doesn't attempt to fix hexchat's own
20//! bugs. It makes no effort to stop you from unloading your own code while it's still running, for
21//! example.
22//!
23//! When using this library, it's strongly recommended to avoid heap-allocated statics (static
24//! mutexes, lazy_static, etc). This is because it's currently impossible to deallocate those on
25//! plugin unload. This can be worked around by placing those statics as fields in your plugin
26//! struct.
27//!
28//! This caveat does not apply to static assets (`static FOO: &'static str`, for example), but it
29//! does apply to thread-local storage.
30//!
31//! # Panics
32//!
33//! Unless otherwise stated, all functions in this crate taking strings (be
34//! that `&str`, `String`, etc) panic when the string contains embedded `NUL`
35//! characters (`\0`).
36//!
37//! # Examples
38//!
39//! ```no_run
40//! #[macro_use]
41//! extern crate hexchat_unsafe_plugin;
42//!
43//! use std::pin::Pin;
44//! use std::sync::Mutex;
45//! use hexchat_unsafe_plugin::{Plugin, PluginHandle, HookHandle};
46//!
47//! #[derive(Default)]
48//! struct MyPlugin<'ph> {
49//! cmd: Mutex<Option<HookHandle<'ph, 'ph>>>
50//! }
51//!
52//! unsafe impl<'ph> Plugin<'ph> for MyPlugin<'ph> {
53//! fn init(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool {
54//! ph.register("myplugin", "0.1.0", "my simple plugin");
55//! *self.cmd.lock().unwrap() = Some(ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| {
56//! ph.print("Hello, World!");
57//! hexchat_unsafe_plugin::EAT_ALL
58//! }));
59//! true
60//! }
61//! }
62//!
63//! hexchat_plugin!('ph, MyPlugin<'ph>);
64//!
65//! # fn main() { } // satisfy the compiler, we can't actually run the code
66//! ```
67
68/*
69 * Some info about how HexChat does things:
70 *
71 * All strings passed across the C API are UTF-8.
72 * - Except `hexchat_get_info(ph, "libdirfs")`, so we need to be careful with
73 * that one.
74 *
75 * The pointers
76 * `name: *mut *const char, desc: *mut *const char, vers: *mut *const char`
77 * point to inside the ph - that is, they're aliased. Thus, we must never
78 * convert a ph to an & or &mut except as part of retrieving or setting values
79 * in it (e.g. `(*ph).hexchat_get_info` or `(*ph).userdata = value`).
80 *
81 * `hexchat_plugin_get_info` is never used, so we omit it. It would be
82 * impractical not to.
83 *
84 * These cause UB:
85 * - `hexchat_command` may invalidate the plugin context.
86 * - `hexchat_find_context` and `hexchat_nickcmp` use the plugin context
87 * without checking it.
88 * - `hexchat_get_prefs` uses the plugin context if name == "state_cursor" or
89 * "id" (or anything with the same hash).
90 * - `hexchat_list_get` uses the plugin context if name == "notify" (or
91 * anything with the same hash).
92 * - `hexchat_list_str`, `hexchat_list_int`,
93 * - `hexchat_emit_print`, `hexchat_emit_print_attrs` use the plugin context.
94 * - `hexchat_send_modes` uses the plugin context.
95 * We need to wrap them (or, alternatively, hexchat_command). However, there's
96 * no (safe) way to get a valid context afterwards.
97 * - Actually that's a lie. Hexchat does a trick to give you a context as part
98 * of the channel list.
99 * We can use that to our advantage. I'm not sure if it's better to wrap
100 * hexchat_command or the other functions, tho.
101 * (Do we want to walk a linked list every time we use hexchat_command?
102 * I'd think hexchat_command is the most used API function... Plus,
103 * emit_print could indirectly invalidate the context as well.)
104 *
105 * `hexchat_send_modes` should only be used with channels; however, it doesn't
106 * cause UB - it just doesn't work.
107 *
108 * `hexchat_pluginpref_get_int`, `hexchat_pluginpref_get_str`,
109 * `hexchat_pluginpref_set_int`, `hexchat_pluginpref_set_str` cannot be used
110 * while `name`, `desc`, `vers` are null.
111 *
112 * `hexchat_plugin_init` receives an arg string. it may be null. this isn't
113 * documented anywhere.
114 *
115 * We can add the "close context" hook as the first thing when registering a
116 * plugin, and invalidate known contexts from it. this is because hexchat
117 * always adds new hooks with the same priority before old hooks.
118 *
119 * Borrowing can be scary. Things that can invalidate borrows must be &mut, but
120 * borrows by themselves are generally fine. Unless they're not.
121 *
122 * Some get_info calls may be unsound on some platforms when threads are
123 * involved, as they use getenv. This should be fixed by libc. We still need to
124 * copy them into a String/CString.
125 */
126
127/*
128 * Some info about how we do things:
129 *
130 * DO NOT CALL printf/commandf/etc FAMILY OF FUNCTIONS. You can't avoid allocations, so just
131 * allocate some CStrings on the Rust side. It has the exact same effect, since those functions
132 * allocate internally anyway.
133 */
134
135/*
136 * Big list o' TODO:
137 * -[ ] Finish API support. [PRI-HIGH]
138 * -[x] word
139 * -[x] word_eol
140 * -[x] HEXCHAT_PRI_{HIGHEST, HIGH, NORM, LOW, LOWEST}
141 * -[x] HEXCHAT_EAT_{NONE, HEXCHAT, PLUGIN, ALL}
142 * -[ ] HEXCHAT_FD_{READ, WRITE, EXCEPTION, NOTSOCKET}
143 * -[x] hexchat_command (for commandf, use command(&format!("...")), it is equivalent.)
144 * -[x] hexchat_print (for printf, use print(&format!("...")), it is equivalent.)
145 * -[x] hexchat_emit_print
146 * -[x] hexchat_emit_print_attrs
147 * -[x] hexchat_send_modes
148 * -[x] hexchat_nickcmp
149 * -[x] hexchat_strip
150 * -[x] ~~hexchat_free~~ not available - use Drop impls.
151 * -[x] ~~hexchat_event_attrs_create~~ not available - converted as needed
152 * -[x] ~~hexchat_event_attrs_free~~ not available - use Drop impls.
153 * -[#] hexchat_get_info (with the below as separate methods)
154 * -[x] libdirfs
155 * -[ ] gtkwin_ptr
156 * -[ ] win_ptr
157 * -[x] hexchat_get_prefs
158 * -[x] hexchat_list_get
159 * -[x] ~~hexchat_list_fields~~ not available. no alternative provided.
160 * -[x] hexchat_list_next
161 * -[x] hexchat_list_str
162 * -[x] hexchat_list_int
163 * -[ ] hexchat_list_time
164 * -[x] ~~hexchat_list_free~~ not available - use Drop impls.
165 * -[x] hexchat_hook_command
166 * -[ ] hexchat_hook_fd
167 * -[x] hexchat_hook_print
168 * -[x] hexchat_hook_print_attrs
169 * -[#] hexchat_hook_server (implemented through _attrs)
170 * -[x] hexchat_hook_server_attrs
171 * -[x] hexchat_hook_timer
172 * -[x] ~~hexchat_unhook~~ not available - use Drop impls
173 * -[x] hexchat_find_context
174 * -[x] hexchat_get_context
175 * -[x] hexchat_set_context
176 * -[x] hexchat_pluginpref_set_str
177 * -[x] hexchat_pluginpref_get_str
178 * -[x] ~~hexchat_pluginpref_set_int~~ not available - use `format!`
179 * -[x] ~~hexchat_pluginpref_get_int~~ not available - use `str::parse`
180 * -[x] hexchat_pluginpref_delete
181 * -[x] hexchat_pluginpref_list
182 * -[x] hexchat_plugingui_add
183 * -[x] ~~hexchat_plugingui_remove~~ not available - use Drop impls.
184 * -[x] Wrap contexts within something we keep track of. Mark them invalid when contexts are
185 * closed.
186 * -[x] Anchor closures on the stack using Rc<T>. Many (most?) hooks are reentrant. As far as I
187 * know, all of them need this.
188 * -[x] Additionally, use a Cell<bool> for timers.
189 * -[ ] ???
190 */
191
192#![cfg_attr(feature="nightly_tests", feature(c_variadic))]
193
194#[macro_use]
195extern crate impl_trait;
196#[doc(hidden)]
197pub extern crate libc;
198
199// private macros
200
201/// Calls a function on a PluginHandle struct.
202macro_rules! ph_call {
203 ($f:ident($ph:expr, $($args:expr),*)) => {
204 ((*$ph.data.ph).$f)($ph.plugin, $($args),*)
205 };
206 ($f:ident($ph:expr $(,)?)) => {
207 ((*$ph.data.ph).$f)($ph.plugin)
208 };
209}
210
211mod eat;
212mod extra_tests;
213mod infoid;
214mod internals;
215pub mod list;
216#[doc(hidden)]
217#[cfg(feature="nightly_tests")]
218pub mod mock;
219mod pluginfo;
220mod strip;
221mod word;
222
223pub use eat::*;
224pub use infoid::InfoId;
225pub use strip::*;
226pub use word::*;
227
228use internals::HexchatEventAttrs as RawAttrs;
229use internals::Ph as RawPh;
230use pluginfo::PluginInfo;
231
232use std::borrow::Cow;
233use std::cell::Cell;
234use std::cell::RefCell;
235use std::collections::HashSet;
236use std::convert::TryInto;
237use std::ffi::{CString, CStr};
238use std::fmt;
239use std::marker::PhantomData;
240use std::mem;
241use std::mem::ManuallyDrop;
242use std::mem::MaybeUninit;
243use std::panic::{AssertUnwindSafe, RefUnwindSafe, UnwindSafe, catch_unwind};
244use std::pin::Pin;
245use std::ptr;
246use std::rc::Rc;
247use std::rc::Weak as RcWeak;
248use std::str::FromStr;
249use std::thread;
250use std::time::{SystemTime, UNIX_EPOCH, Duration};
251
252#[doc(hidden)]
253pub use libc::{c_char, c_int, c_void, time_t};
254
255// ****** //
256// PUBLIC //
257// ****** //
258
259// Consts
260
261// PRI_*
262/// Equivalent to HEXCHAT_PRI_HIGHEST
263pub const PRI_HIGHEST: i32 = 127;
264/// Equivalent to HEXCHAT_PRI_HIGH
265pub const PRI_HIGH: i32 = 64;
266/// Equivalent to HEXCHAT_PRI_NORM
267pub const PRI_NORM: i32 = 0;
268/// Equivalent to HEXCHAT_PRI_LOW
269pub const PRI_LOW: i32 = -64;
270/// Equivalent to HEXCHAT_PRI_LOWEST
271pub const PRI_LOWEST: i32 = -128;
272
273// Traits
274
275/// A hexchat plugin.
276///
277/// # Safety
278///
279/// Modern operating systems cannot deal with dynamic unloading when threads
280/// are involved, because we still haven't figured out how to track which code
281/// address started a syscall that spawned a thread for some reason, so there's
282/// no way for the dynamic loader to stop those threads when unloading.
283///
284/// *Fortunately* we can just shift that responsibility onto the unsuspecting
285/// Rust user. Because Rust is a safe language, this makes writing plugins
286/// inherently unsafe!
287///
288/// At least Unsafe Rust is still safer than writing C. So you have that going.
289///
290/// TL;DR: Either don't use threads, or ensure they're dead in `Drop`/`deinit`.
291pub unsafe trait Plugin<'ph> {
292 /// Called to initialize the plugin.
293 fn init(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool;
294
295 /// Called to deinitialize the plugin.
296 ///
297 /// This is always called immediately prior to Drop::drop.
298 ///
299 /// # A note about unwinding
300 ///
301 /// Panics in deinit will prevent the plugin from being correctly unloaded!
302 /// Be careful!
303 fn deinit(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>) {
304 let _ = ph;
305 }
306}
307
308// Structs
309
310/// A `*mut RawPh` with a lifetime bolted to it.
311///
312/// This allows us to enforce a non-`'static` lifetime on the `Plugin`.
313// this is NOT public API
314#[doc(hidden)]
315#[repr(transparent)]
316#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
317pub struct LtPhPtr<'ph> {
318 ph: *mut RawPh,
319 // 'ph has to be invariant because RawPh is self-referential.
320 // "ideally" we'd want `&'ph mut RawPh<'ph>`, tho the `*mut` above would've
321 // had the same effect if `RawPh` were `RawPh<'ph>`.
322 _lt: PhantomData<fn(&'ph ()) -> &'ph ()>,
323}
324
325/// A hexchat plugin handle, used to register hooks and interact with hexchat.
326///
327/// # Examples
328///
329/// ```no_run
330/// use hexchat_unsafe_plugin::{PluginHandle};
331///
332/// fn init(ph: &mut PluginHandle) {
333/// ph.register("myplug", "0.1.0", "my awesome plug");
334/// }
335/// ```
336pub struct PluginHandle<'ph> {
337 data: LtPhPtr<'ph>,
338 plugin: *mut RawPh,
339 contexts: Contexts,
340 // Used for registration
341 info: PluginInfo,
342}
343
344/// A setting value, returned by [`ValidContext::get_prefs`].
345#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
346pub enum PrefValue {
347 String(String),
348 Int(i32),
349 Bool(bool),
350}
351
352mod valid_context {
353 use std::ptr;
354
355 use crate::list;
356 use crate::PluginHandle;
357
358 /// A `PluginHandle` operating on a valid context.
359 ///
360 /// Methods that require a valid plugin context are only available through
361 /// this. Methods that may invalidate the plugin context also consume this.
362 ///
363 /// See also [`PluginHandle::ensure_valid_context`].
364 pub struct ValidContext<'a, 'ph: 'a> {
365 // NOTE: NOT mut(!)
366 pub(crate) ph: &'a PluginHandle<'ph>,
367 fields: list::Fields<'a, 'ph, list::Contexts>,
368 _hidden: (),
369 }
370
371 impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
372 /// Wraps a PluginHandle in a ValidContext.
373 ///
374 /// # Safety
375 ///
376 /// The PluginHandle's context must be valid.
377 pub(crate) unsafe fn new(ph: &'a mut PluginHandle<'ph>) -> Self {
378 static CONTEXTS: list::Contexts = list::Contexts;
379 Self {
380 ph: &*ph,
381 fields: list::Fields {
382 context: &*ph,
383 // this isn't actually documented but hexchat explicitly
384 // supports passing in NULL for the current context.
385 // this gives access to some properties that aren't
386 // available through get_info.
387 list: ptr::null_mut(),
388 _t: &CONTEXTS,
389 },
390 _hidden: (),
391 }
392 }
393 }
394
395 impl<'a, 'ph: 'a> std::ops::Deref for ValidContext<'a, 'ph> {
396 type Target = list::Fields<'a, 'ph, list::Contexts>;
397 fn deref(&self) -> &Self::Target {
398 &self.fields
399 }
400 }
401}
402pub use valid_context::ValidContext;
403
404/// Event attributes.
405// TODO better docs.
406#[derive(Clone)]
407pub struct EventAttrs<'a> {
408 /// Server time.
409 #[deprecated(since="2.3.0", note="Use raw_server_time instead")]
410 pub server_time: Option<SystemTime>,
411 /// Raw server time. Only does anything if `use_raw_server_time` is set.
412 pub raw_server_time: libc::time_t,
413 /// Whether to pass `server_time` or `raw_server_time` to hexchat.
414 pub use_raw_server_time: bool,
415 _dummy: PhantomData<&'a ()>,
416}
417
418/// A hook handle.
419///
420/// Likes to get caught on stuff. Unhooks when dropped.
421#[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"]
422pub struct HookHandle<'ph, 'f> where 'f: 'ph {
423 ph: LtPhPtr<'ph>,
424 hh: *const internals::HexchatHook,
425 freed: Rc<Cell<bool>>,
426 // this does actually store an Rc<...>, but on the other side of the FFI.
427 _f: PhantomData<Rc<HookUd<'f>>>,
428}
429
430/// A virtual plugin.
431#[must_use = "Virtual plugins must be stored somewhere and are automatically unregistered on Drop"]
432pub struct PluginEntryHandle<'ph> {
433 ph: LtPhPtr<'ph>,
434 entry: *const internals::PluginGuiHandle,
435}
436
437/// A context.
438#[derive(Clone)]
439pub struct Context<'ph> {
440 contexts: Contexts,
441 ctx: RcWeak<*const internals::HexchatContext>,
442 _ph: PhantomData<&'ph RawPh>,
443}
444
445/// The error returned by [`PluginHandle::with_context`] when the context is
446/// not valid.
447// #[derive(Debug)] // doesn't work
448pub struct InvalidContextError<F>(F);
449
450/// An iterable list of plugin pref names.
451pub struct PluginPrefList {
452 inner: Vec<u8>,
453}
454
455/// An iterator over `PluginPrefList`.
456pub struct PluginPrefListIter<'a> {
457 list: Option<&'a [u8]>,
458}
459
460// Enums
461
462/// Errors returned by `pluginpref_*` functions.
463#[derive(Debug)]
464pub enum PluginPrefError {
465 /// The var contains a forbidden character: `[ ,=\n]` (space, comma,
466 /// equals, newline), is too long (max 126 bytes), is empty, or contains
467 /// trailing ASCII whitespace.
468 ///
469 /// Returned by anything that interacts with vars. `pluginpref_list` only
470 /// returns these during iteration.
471 InvalidVar,
472 /// The value starts with space.
473 ///
474 /// Returned by `pluginpref_set`.
475 InvalidValue,
476 /// The input (var + value) was too long after encoding.
477 ///
478 /// Returned by `pluginpref_set`.
479 TooLong,
480 /// The returned value was not valid UTF-8.
481 ///
482 /// Returned by `pluginpref_get`.
483 Utf8Error(::std::ffi::IntoStringError),
484 /// The returned var was not valid UTF-8.
485 ///
486 /// Returned while iterating a `pluginpref_list`.
487 VarUtf8Error(::std::str::Utf8Error),
488 /// The operation failed.
489 ///
490 /// Returned by anything that interacts with pluginprefs. Iterating a
491 /// `pluginpref_list` never returns this.
492 Failed,
493}
494
495// ***** //
496// impls //
497// ***** //
498
499impl<'a> Iterator for PluginPrefListIter<'a> {
500 type Item = Result<&'a str, PluginPrefError>;
501
502 fn next(&mut self) -> Option<Self::Item> {
503 let mut splitter = self.list?.splitn(2, |x| *x == 0);
504 let ret = splitter.next().unwrap();
505 let ret = test_pluginpref_var(ret).and_then(|_| {
506 std::str::from_utf8(ret).map_err(|e| PluginPrefError::VarUtf8Error(e))
507 });
508 self.list = splitter.next();
509 Some(ret)
510 }
511}
512
513impl<'a> IntoIterator for &'a PluginPrefList {
514 type Item = Result<&'a str, PluginPrefError>;
515
516 type IntoIter = PluginPrefListIter<'a>;
517
518 fn into_iter(self) -> Self::IntoIter {
519 PluginPrefListIter {
520 list: self.inner.split_last().map(|(_, x)| x),
521 }
522 }
523}
524
525impl<F> InvalidContextError<F> {
526 /// Returns the closure wrapped within this error.
527 pub fn get_closure(self) -> F {
528 self.0
529 }
530}
531
532impl_trait! {
533 impl PrefValue {
534 /// Projects this `PrefValue` as `&str`, if it is a `String`. Returns
535 /// `None` otherwise.
536 pub fn as_str(&self) -> Option<&str> {
537 match self {
538 &Self::String(ref s) => Some(s),
539 _ => None,
540 }
541 }
542
543 /// Projects this `PrefValue` as `i32`, if it is an `Int`. Returns
544 /// `None` otherwise.
545 pub fn as_i32(&self) -> Option<i32> {
546 match self {
547 &Self::Int(i) => Some(i),
548 _ => None,
549 }
550 }
551
552 /// Projects this `PrefValue` as `bool`, if it is a `Bool`. Returns
553 /// `None` otherwise.
554 pub fn as_bool(&self) -> Option<bool> {
555 match self {
556 &Self::Bool(b) => Some(b),
557 _ => None,
558 }
559 }
560
561 /// Consumes this `PrefValue` and attempts to take a `String` out of
562 /// it.
563 pub fn into_string(self) -> Option<String> {
564 match self {
565 Self::String(s) => Some(s),
566 _ => None,
567 }
568 }
569
570 impl trait From<String> {
571 fn from(s: String) -> Self {
572 Self::String(s)
573 }
574 }
575 impl trait From<i32> {
576 fn from(i: i32) -> Self {
577 Self::Int(i)
578 }
579 }
580 impl trait From<bool> {
581 fn from(b: bool) -> Self {
582 Self::Bool(b)
583 }
584 }
585 }
586}
587
588impl<'ph, 'f> HookHandle<'ph, 'f> where 'f: 'ph {
589 /// If this is a timer hook, returns whether the hook has expired.
590 ///
591 /// Otherwise, returns false.
592 ///
593 /// # Examples
594 ///
595 /// ```no_run
596 /// use hexchat_unsafe_plugin::{HookHandle};
597 ///
598 /// /// Remove timers that have expired.
599 /// fn clean_up_timers(timers: &mut Vec<HookHandle<'_, '_>>) {
600 /// timers.retain(|timer| {
601 /// !timer.expired()
602 /// });
603 /// }
604 /// ```
605 pub fn expired(&self) -> bool {
606 self.freed.get()
607 }
608}
609
610impl<'ph, 'f> Drop for HookHandle<'ph, 'f> where 'f: 'ph {
611 fn drop(&mut self) {
612 if self.freed.get() {
613 // already free'd.
614 return;
615 }
616 self.freed.set(true);
617 unsafe {
618 let b = ((*self.ph.ph).hexchat_unhook)(self.ph.ph, self.hh) as *mut HookUd<'f>;
619 // we assume b is not null. this should be safe.
620 // just drop it
621 drop(Rc::from_raw(b));
622 }
623 }
624}
625
626impl<'ph> Drop for PluginEntryHandle<'ph> {
627 fn drop(&mut self) {
628 unsafe {
629 ((*self.ph.ph).hexchat_plugingui_remove)(self.ph.ph, self.entry);
630 }
631 }
632}
633
634impl_trait! {
635 impl<'ph> Context<'ph> {
636 impl trait UnwindSafe {}
637 impl trait RefUnwindSafe {}
638 impl trait Drop {
639 fn drop(&mut self) {
640 // check if we need to clean anything up
641 if self.ctx.strong_count() == 1 && self.ctx.weak_count() == 1 {
642 let strong = self.ctx.upgrade().unwrap();
643 self.contexts.borrow_mut().remove(&strong);
644 }
645 }
646 }
647 }
648}
649
650/// Logs a panic message.
651///
652/// # Safety
653///
654/// `ph` must be a valid pointer (see `std::ptr::read`).
655unsafe fn log_panic(ph: *mut RawPh, e: Box<dyn std::any::Any + Send + 'static>) {
656 // if it's a &str or String, just print it
657 if let Some(s) = e.downcast_ref::<&str>() {
658 hexchat_print_str(ph, s, false);
659 } else if let Some(s) = e.downcast_ref::<String>() {
660 hexchat_print_str(ph, &s, false);
661 } else if let Some(s) = e.downcast_ref::<Cow<'static, str>>() {
662 hexchat_print_str(ph, &s, false);
663 } else {
664 hexchat_print_str(ph, "couldn't log panic message", false);
665 if let Err(e) = catch_unwind(AssertUnwindSafe(|| drop(e))) {
666 // eprintln panics, hexchat_print_str doesn't.
667 hexchat_print_str(ph, "ERROR: panicked while trying to log panic!", false);
668 mem::forget(e);
669 std::process::abort();
670 }
671 }
672}
673
674/// Handles a hook panic at the C-Rust ABI boundary.
675///
676/// # Safety
677///
678/// `ph` must be a valid pointer (see `std::ptr::read`).
679unsafe fn call_hook_protected<F: FnOnce() -> Eat + UnwindSafe>(
680 ph: *mut RawPh,
681 f: F
682) -> Eat {
683 match catch_unwind(f) {
684 Result::Ok(v @ _) => v,
685 Result::Err(e @ _) => {
686 log_panic(ph, e);
687 EAT_NONE
688 }
689 }
690}
691
692impl<'ph> PluginHandle<'ph> {
693 /// Wraps the raw handle.
694 ///
695 /// # Safety
696 ///
697 /// `ph` must be a valid pointer (see `std::ptr::read`).
698 unsafe fn new(data: LtPhPtr<'ph>, info: PluginInfo, contexts: Contexts) -> PluginHandle<'ph> {
699 PluginHandle {
700 data, plugin: data.ph, info, contexts
701 }
702 }
703
704 /// Registers this hexchat plugin. This must be called exactly once when the plugin is loaded.
705 ///
706 /// # Panics
707 ///
708 /// This function panics if this plugin is already registered.
709 ///
710 /// # Examples
711 ///
712 /// ```no_run
713 /// use hexchat_unsafe_plugin::PluginHandle;
714 ///
715 /// fn init(ph: &mut PluginHandle<'_>) {
716 /// ph.register("foo", "0.1.0", "my foo plugin");
717 /// }
718 /// ```
719 pub fn register(&mut self, name: &str, desc: &str, ver: &str) {
720 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered");
721 unsafe {
722 let info = self.info;
723 if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
724 panic!("Attempt to re-register a plugin");
725 }
726 let name = CString::new(name).unwrap();
727 let desc = CString::new(desc).unwrap();
728 let ver = CString::new(ver).unwrap();
729 // these shouldn't panic. if they do, we'll need to free them afterwards.
730 (*info.name) = name.into_raw();
731 (*info.desc) = desc.into_raw();
732 (*info.vers) = ver.into_raw();
733 }
734 }
735
736 /// Returns this plugin's registered name.
737 ///
738 /// # Panics
739 ///
740 /// This function panics if this plugin is not registered.
741 pub fn get_name(&self) -> &str {
742 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered");
743 unsafe {
744 let info = self.info;
745 if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
746 std::str::from_utf8_unchecked(CStr::from_ptr(*info.name).to_bytes())
747 } else {
748 panic!("Attempt to get the name of a plugin that was not yet registered.");
749 }
750 }
751 }
752
753 /// Returns this plugin's registered description.
754 ///
755 /// # Panics
756 ///
757 /// This function panics if this plugin is not registered.
758 pub fn get_description(&self) -> &str {
759 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered");
760 unsafe {
761 let info = self.info;
762 if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
763 std::str::from_utf8_unchecked(CStr::from_ptr(*info.desc).to_bytes())
764 } else {
765 panic!("Attempt to get the description of a plugin that was not yet registered.");
766 }
767 }
768 }
769
770 /// Returns this plugin's registered version.
771 ///
772 /// # Panics
773 ///
774 /// This function panics if this plugin is not registered.
775 pub fn get_version(&self) -> &str {
776 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered");
777 unsafe {
778 let info = self.info;
779 if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
780 std::str::from_utf8_unchecked(CStr::from_ptr(*info.vers).to_bytes())
781 } else {
782 panic!("Attempt to get the version of a plugin that was not yet registered.");
783 }
784 }
785 }
786
787 /// Ensures the current context is valid.
788 ///
789 /// # Panics
790 ///
791 /// This function may panic if it's called while hexchat is closing.
792 pub fn ensure_valid_context<F, R>(&mut self, f: F) -> R where F: for<'a> FnOnce(ValidContext<'a, 'ph>) -> R {
793 let ctx = self.get_context();
794 let res = self.with_context(&ctx, f);
795 match res {
796 Result::Ok(r @ _) => r,
797 Result::Err(e @ _) => {
798 let nctx = self.find_valid_context().expect("ensure_valid_context failed (find_valid_context failed), was hexchat closing?");
799 self.with_context(&nctx, e.get_closure()).ok().expect("ensure_valid_context failed, was hexchat closing?")
800 }
801 }
802 }
803
804 /// Operates on a virtual plugin.
805 ///
806 /// # Examples
807 ///
808 /// ```no_run
809 /// use hexchat_unsafe_plugin::{PluginHandle};
810 ///
811 /// fn contexts_can_be_passed_around(ph: &mut PluginHandle<'_>) {
812 /// let ctx = ph.get_context();
813 /// let mut vplug = ph.plugingui_add("foo", "bar", "baz", "qux");
814 /// ph.for_entry_mut(&mut vplug, |ph| {
815 /// ph.set_context(&ctx);
816 /// ph.get_context()
817 /// });
818 /// }
819 /// ```
820 pub fn for_entry_mut<F, R>(&mut self, entry: &mut PluginEntryHandle<'ph>, f: F) -> R where F: FnOnce(&mut PluginHandle<'ph>) -> R {
821 // we're doing something kinda (very) weird here, but this is the only
822 // way to get and set pluginprefs for virtual plugins. (not that we
823 // support those yet...)
824 // this should be sound?
825 let data = self.data;
826 let info = self.info;
827 let contexts = Rc::clone(&self.contexts);
828 let mut handle = unsafe { Self::new(data, info, contexts) };
829 handle.plugin = entry.entry as *mut RawPh;
830 handle.set_context(&self.get_context());
831 f(&mut handle)
832 }
833
834 /// Registers a virtual plugin.
835 pub fn plugingui_add(&self, filename: &str, name: &str, description: &str, version: &str) -> PluginEntryHandle<'ph> {
836 let filename = CString::new(filename).unwrap();
837 let name = CString::new(name).unwrap();
838 let description = CString::new(description).unwrap();
839 let version = CString::new(version).unwrap();
840 let res = unsafe {
841 ph_call!(hexchat_plugingui_add(self, filename.as_ptr(), name.as_ptr(), description.as_ptr(), version.as_ptr(), ptr::null_mut()))
842 };
843 PluginEntryHandle { ph: self.data, entry: res }
844 }
845
846 /// Returns the current context.
847 ///
848 /// Note: The returned context may be invalid. Use [`Self::set_context`] to
849 /// check.
850 pub fn get_context(&self) -> Context<'ph> {
851 let ctxp = unsafe { ph_call!(hexchat_get_context(self)) };
852 // This needs to be fixed by hexchat. I cannot make the value become
853 // null when it's invalid without invoking UB. This is because I can't
854 // set_context to null.
855 let ok = unsafe { ph_call!(hexchat_set_context(self, ctxp)) };
856 unsafe { wrap_context(self, if ok == 0 { ptr::null() } else { ctxp }) }
857 }
858
859 /// Sets the current context.
860 ///
861 /// Returns `true` if the context is valid, `false` otherwise.
862 pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool {
863 // we could've made this &self but we didn't. avoid breaking API.
864 self.set_context_internal(ctx)
865 }
866
867 /// Do something in a valid context.
868 ///
869 /// Note that this changes the active context and doesn't change it back
870 /// (but that should be fine for most use-cases).
871 ///
872 /// # Errors
873 ///
874 /// Returns `Err(InvalidContextError(f))` if the context is invalid. See
875 /// [`set_context`]. Otherwise, returns `Ok(f(...))`.
876 ///
877 /// Note that `InvalidContextError` contains the original closure. This
878 /// allows you to retry, if you so wish.
879 ///
880 /// [`set_context`]: #method.set_context
881 #[inline]
882 pub fn with_context<F, R>(&mut self, ctx: &Context<'ph>, f: F) -> Result<R, InvalidContextError<F>> where F: for<'a> FnOnce(ValidContext<'a, 'ph>) -> R {
883 if !self.set_context(ctx) {
884 Err(InvalidContextError(f))
885 } else {
886 Ok(f(unsafe { ValidContext::new(self) }))
887 }
888 }
889
890 /// Sets a command hook.
891 ///
892 /// # Panics
893 ///
894 /// Panics if this is a borrowed [`PluginEntryHandle`].
895 ///
896 /// # Examples
897 ///
898 /// ```no_run
899 /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
900 ///
901 /// fn register_commands<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
902 /// vec![
903 /// ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| {
904 /// ph.print("Hello, World!");
905 /// hexchat_unsafe_plugin::EAT_ALL
906 /// }),
907 /// ]
908 /// }
909 /// ```
910 pub fn hook_command<'f, F>(&self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
911 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
912 unsafe extern "C" fn callback(word: *const *const c_char, word_eol: *const *const c_char, ud: *mut c_void) -> c_int {
913 let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
914 (f)(word, word_eol, ptr::null()).do_eat as c_int
915 }
916 let b: Rc<HookUd> = {
917 let ph = self.data;
918 let info = self.info;
919 let contexts = Rc::clone(&self.contexts);
920 Rc::new(Box::new(move |word, word_eol, _| {
921 let cb = &cb;
922 let contexts = Rc::clone(&contexts);
923 unsafe {
924 call_hook_protected(ph.ph, move || {
925 let mut ph = PluginHandle::new(ph, info, contexts);
926 let word = Word::new(word);
927 let word_eol = WordEol::new(word_eol);
928 cb(&mut ph, word, word_eol)
929 })
930 }
931 }))
932 };
933 let name = CString::new(cmd).unwrap();
934 let help_text = help.map(CString::new).map(Result::unwrap);
935 let bp = Rc::into_raw(b);
936 unsafe {
937 let res = ph_call!(hexchat_hook_command(self, name.as_ptr(), pri as c_int, callback, help_text.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()), bp as *mut _));
938 assert!(!res.is_null());
939 HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData }
940 }
941 }
942 /// Sets a server hook.
943 ///
944 /// # Examples
945 ///
946 /// ```no_run
947 /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
948 ///
949 /// fn register_server_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
950 /// vec![
951 /// ph.hook_server("PRIVMSG", hexchat_unsafe_plugin::PRI_NORM, |ph, word, word_eol| {
952 /// if word.len() > 0 && word[0].starts_with('@') {
953 /// ph.print("We have message tags!?");
954 /// }
955 /// hexchat_unsafe_plugin::EAT_NONE
956 /// }),
957 /// ]
958 /// }
959 /// ```
960 pub fn hook_server<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
961 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
962 self.hook_server_attrs(cmd, pri, move |ph, w, we, _| cb(ph, w, we))
963 }
964 /// Sets a server hook, with attributes.
965 pub fn hook_server_attrs<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
966 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
967 unsafe extern "C" fn callback(word: *const *const c_char, word_eol: *const *const c_char, attrs: *const RawAttrs, ud: *mut c_void) -> c_int {
968 let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
969 (f)(word, word_eol, attrs).do_eat as c_int
970 }
971 let b: Rc<HookUd> = {
972 let ph = self.data;
973 let info = self.info;
974 let contexts = Rc::clone(&self.contexts);
975 Rc::new(Box::new(move |word, word_eol, attrs| {
976 let cb = &cb;
977 let contexts = Rc::clone(&contexts);
978 unsafe {
979 call_hook_protected(ph.ph, move || {
980 let mut ph = PluginHandle::new(ph, info, contexts);
981 let word = Word::new(word);
982 let word_eol = WordEol::new(word_eol);
983 let attrs = (&*attrs).into();
984 cb(&mut ph, word, word_eol, attrs)
985 })
986 }
987 }))
988 };
989 let name = CString::new(cmd).unwrap();
990 let bp = Rc::into_raw(b);
991 unsafe {
992 let res = ph_call!(hexchat_hook_server_attrs(self, name.as_ptr(), pri as c_int, callback, bp as *mut _));
993 assert!(!res.is_null());
994 HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData }
995 }
996 }
997 /// Sets a print hook.
998 ///
999 /// # Examples
1000 ///
1001 /// ```no_run
1002 /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
1003 ///
1004 /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
1005 /// vec![
1006 /// ph.hook_print("Channel Message", hexchat_unsafe_plugin::PRI_NORM, |ph, arg| {
1007 /// if let Some(nick) = arg.get(0) {
1008 /// if *nick == "KnOwN_SpAmMeR" {
1009 /// return hexchat_unsafe_plugin::EAT_ALL
1010 /// }
1011 /// }
1012 /// hexchat_unsafe_plugin::EAT_NONE
1013 /// }),
1014 /// ]
1015 /// }
1016 /// ```
1017 pub fn hook_print<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
1018 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
1019 // hmm, is there any way to avoid this code duplication?
1020 // hook_print is special because dummy prints (keypresses, Close Context) are handled
1021 // through here, but never through hook_print_attrs. :/
1022 unsafe extern "C" fn callback(word: *const *const c_char, ud: *mut c_void) -> c_int {
1023 let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
1024 (f)(word, ptr::null(), ptr::null()).do_eat as c_int
1025 }
1026 // we use "Close Context" to clean up dangling pointers.
1027 // hexchat lets plugins/hooks eat "Close Context" from eachother (tho
1028 // not from hexchat, for good reason), so this can still be unsound
1029 // if other plugins eat "Close Context". in those cases, we'd consider
1030 // those plugins to be faulty, tho it'd be nice if hexchat handled it.
1031 // we still do our best to be well-behaved.
1032 let suppress_eat = name.eq_ignore_ascii_case("Close Context");
1033 let b: Rc<HookUd> = {
1034 let ph = self.data;
1035 let info = self.info;
1036 let contexts = Rc::clone(&self.contexts);
1037 Rc::new(Box::new(move |word, _, _| {
1038 let cb = &cb;
1039 let contexts = Rc::clone(&contexts);
1040 unsafe {
1041 call_hook_protected(ph.ph, move || {
1042 let mut ph = PluginHandle::new(ph, info, contexts);
1043 let word = Word::new(word);
1044 match cb(&mut ph, word) {
1045 _ if suppress_eat => EAT_NONE,
1046 eat => eat,
1047 }
1048 })
1049 }
1050 }))
1051 };
1052 let name = CString::new(name).unwrap();
1053 let bp = Rc::into_raw(b);
1054 unsafe {
1055 let res = ph_call!(hexchat_hook_print(self, name.as_ptr(), pri as c_int, callback, bp as *mut _));
1056 assert!(!res.is_null());
1057 HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData }
1058 }
1059 }
1060 /// Sets a print hook, with attributes.
1061 ///
1062 /// # Examples
1063 ///
1064 /// ```no_run
1065 /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
1066 ///
1067 /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
1068 /// vec![
1069 /// ph.hook_print_attrs("Channel Message", hexchat_unsafe_plugin::PRI_NORM, |ph, arg, attrs| {
1070 /// if let Some(nick) = arg.get(0) {
1071 /// if *nick == "KnOwN_SpAmMeR" {
1072 /// return hexchat_unsafe_plugin::EAT_ALL
1073 /// }
1074 /// }
1075 /// hexchat_unsafe_plugin::EAT_NONE
1076 /// }),
1077 /// ]
1078 /// }
1079 /// ```
1080 pub fn hook_print_attrs<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
1081 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
1082 unsafe extern "C" fn callback(word: *const *const c_char, attrs: *const RawAttrs, ud: *mut c_void) -> c_int {
1083 let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
1084 (f)(word, ptr::null(), attrs).do_eat as c_int
1085 }
1086 let b: Rc<HookUd> = {
1087 let ph = self.data;
1088 let info = self.info;
1089 let contexts = Rc::clone(&self.contexts);
1090 Rc::new(Box::new(move |word, _, attrs| {
1091 let cb = &cb;
1092 let contexts = Rc::clone(&contexts);
1093 unsafe {
1094 call_hook_protected(ph.ph, move || {
1095 let mut ph = PluginHandle::new(ph, info, contexts);
1096 let word = Word::new(word);
1097 let attrs = (&*attrs).into();
1098 cb(&mut ph, word, attrs)
1099 })
1100 }
1101 }))
1102 };
1103 let name = CString::new(name).unwrap();
1104 let bp = Rc::into_raw(b);
1105 unsafe {
1106 let res = ph_call!(hexchat_hook_print_attrs(self, name.as_ptr(), pri as c_int, callback, bp as *mut _));
1107 assert!(!res.is_null());
1108 HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData }
1109 }
1110 }
1111 /// Sets a timer hook
1112 ///
1113 /// # Examples
1114 ///
1115 /// ```no_run
1116 /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
1117 ///
1118 /// fn register_timers<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
1119 /// vec![
1120 /// ph.hook_timer(2000, |ph| {
1121 /// ph.print("timer up!");
1122 /// false
1123 /// }),
1124 /// ]
1125 /// }
1126 /// ```
1127 pub fn hook_timer<'f, F>(&self, timeout: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'f + RefUnwindSafe, 'f: 'ph {
1128 assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
1129 unsafe extern "C" fn callback(ud: *mut c_void) -> c_int {
1130 let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
1131 (f)(ptr::null(), ptr::null(), ptr::null()).do_eat as c_int
1132 }
1133 let freed = Rc::new(Cell::new(false));
1134 // helps us clean up the thing when returning false
1135 let dropper = Rc::new(Cell::new(None));
1136 let b: Rc<HookUd> = {
1137 let ph = self.data;
1138 let info = self.info;
1139 let contexts = Rc::clone(&self.contexts);
1140 let freed = AssertUnwindSafe(Rc::clone(&freed));
1141 let dropper = AssertUnwindSafe(Rc::clone(&dropper));
1142 Rc::new(Box::new(move |_, _, _| {
1143 let cb = &cb;
1144 let contexts = Rc::clone(&contexts);
1145 let res = unsafe {
1146 call_hook_protected(ph.ph, move || {
1147 let mut ph = PluginHandle::new(ph, info, contexts);
1148 if cb(&mut ph) {
1149 EAT_HEXCHAT
1150 } else {
1151 EAT_NONE
1152 }
1153 })
1154 };
1155 if res == EAT_NONE && !freed.get() {
1156 freed.set(true);
1157 unsafe {
1158 // drop may panic
1159 // and so may unwrap
1160 // but we must not panic
1161 // (kinda silly to abuse call_hook_protected here
1162 // but hey, it works and it helps with stuff)
1163 call_hook_protected(ph.ph, || {
1164 drop(Rc::from_raw(dropper.take().unwrap()));
1165 EAT_NONE
1166 });
1167 }
1168 }
1169 res
1170 }))
1171 };
1172 let bp = Rc::into_raw(b);
1173 dropper.set(Some(bp));
1174 unsafe {
1175 let res = ph_call!(hexchat_hook_timer(self, timeout as c_int, callback, bp as *mut _));
1176 assert!(!res.is_null());
1177 HookHandle { ph: self.data, hh: res, freed: freed, _f: PhantomData }
1178 }
1179 }
1180
1181 /// Prints to the hexchat buffer.
1182 // this checks the context internally. if it didn't, it wouldn't be safe to
1183 // have here.
1184 pub fn print<T: ToString>(&self, s: T) {
1185 let s = s.to_string();
1186 unsafe {
1187 hexchat_print_str(self.data.ph, &*s, true);
1188 }
1189 }
1190
1191 /// Prints to this hexchat buffer.
1192 ///
1193 /// Glue for usage of the [`write!`] macro with hexchat.
1194 ///
1195 /// # Panics
1196 ///
1197 /// This panics if any broken formatting trait implementations are used in
1198 /// the format arguments. See also [`format!`].
1199 ///
1200 /// # Examples
1201 ///
1202 /// ```no_run
1203 /// use hexchat_unsafe_plugin::PluginHandle;
1204 ///
1205 /// fn hello_world(ph: &mut PluginHandle<'_>) {
1206 /// write!(ph, "Hello, world!");
1207 /// }
1208 /// ```
1209 pub fn write_fmt(&self, fmt: fmt::Arguments<'_>) {
1210 if let Some(s) = fmt.as_str() {
1211 // "fast" path. hexchat_print_str still has to allocate, and
1212 // hexchat is slow af.
1213 unsafe {
1214 hexchat_print_str(self.data.ph, s, true);
1215 }
1216 } else {
1217 self.print(fmt);
1218 }
1219 }
1220
1221 /// Returns context and client information.
1222 ///
1223 /// See [`InfoId`] for the kinds of information that can be retrieved.
1224 ///
1225 /// # Examples
1226 ///
1227 /// ```no_run
1228 /// use hexchat_unsafe_plugin::{InfoId, PluginHandle};
1229 ///
1230 /// /// Returns whether we are currently away.
1231 /// fn is_away(ph: &mut PluginHandle<'_>) -> bool {
1232 /// ph.get_info(InfoId::Away).is_some()
1233 /// }
1234 /// ```
1235 pub fn get_info<'a>(&'a self, id: InfoId) -> Option<Cow<'a, str>> {
1236 let id_cstring = CString::new(&*id.name()).unwrap();
1237 unsafe {
1238 let res = ph_call!(hexchat_get_info(self, id_cstring.as_ptr()));
1239 if res.is_null() {
1240 None
1241 } else {
1242 let s = CStr::from_ptr(res).to_str();
1243 // FIXME: figure out which InfoId's are safe to borrow.
1244 Some(s.expect("broken hexchat").to_owned().into())
1245 }
1246 }
1247 }
1248
1249 /// Returns the path to the plugin directory, used for auto-loading
1250 /// plugins.
1251 ///
1252 /// The returned `CStr` is not guaranteed to be valid UTF-8, but local
1253 /// file system encoding.
1254 ///
1255 /// # Examples
1256 ///
1257 /// ```no_run
1258 /// use std::path::PathBuf;
1259 ///
1260 /// use hexchat_unsafe_plugin::PluginHandle;
1261 ///
1262 /// // On Unix, we can treat this as an array-of-bytes filesystem path.
1263 /// #[cfg(unix)]
1264 /// fn plugin_dir(ph: &PluginHandle<'_>) -> PathBuf {
1265 /// use std::ffi::OsString;
1266 /// use std::os::unix::ffi::OsStringExt;
1267 ///
1268 /// let libdirfs = ph.get_libdirfs().expect("should exist");
1269 /// OsString::from_vec(libdirfs.into_bytes()).into()
1270 /// }
1271 /// ```
1272 pub fn get_libdirfs(&self) -> Option<CString> {
1273 let id_cstring = CString::new("libdirfs").unwrap();
1274 unsafe {
1275 let res = ph_call!(hexchat_get_info(self, id_cstring.as_ptr()));
1276 if res.is_null() {
1277 None
1278 } else {
1279 Some(CStr::from_ptr(res).to_owned())
1280 }
1281 }
1282 }
1283
1284 /// Strips attributes from text. See [`Strip`] for which attributes can
1285 /// be stripped.
1286 ///
1287 /// # Examples
1288 ///
1289 /// ```no_run
1290 /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
1291 ///
1292 /// /// Removes colors
1293 /// fn strip_colors(ph: &PluginHandle<'_>, s: &str) -> String {
1294 /// ph.strip(s, Strip::new().colors(true))
1295 /// }
1296 /// ```
1297 #[cfg(feature = "nul_in_strip")]
1298 pub fn strip(&self, s: &str, strip: Strip) -> String {
1299 // ironically the single API where we can pass embedded NULs.
1300 // we also don't need to worry about terminating it with NUL.
1301 let mut out = Vec::with_capacity(s.len());
1302 let in_ptr = s.as_ptr() as *const _;
1303 let in_len = s.len().try_into().unwrap();
1304 let flags = strip.flags();
1305 // NOTE: avoid panicking from here.
1306 let stripped = unsafe {
1307 ph_call!(hexchat_strip(self, in_ptr, in_len, flags))
1308 };
1309 // tho annoyingly we don't know the out len, so we need to count NULs.
1310 let in_nuls = s.as_bytes().into_iter().filter(|&&x| x == 0).count();
1311 let mut out_len = 0;
1312 for _ in 0..=in_nuls {
1313 while unsafe { *stripped.add(out_len) } != 0 {
1314 out_len += 1;
1315 }
1316 out_len += 1;
1317 }
1318 out.extend_from_slice(unsafe {
1319 // take out the extra NUL at the end.
1320 std::slice::from_raw_parts(stripped as *const _, out_len - 1)
1321 });
1322 unsafe {
1323 ph_call!(hexchat_free(self, stripped as *const _));
1324 }
1325 // we can panic again here, tho this should never panic.
1326 String::from_utf8(out).unwrap()
1327 }
1328
1329 /// Strips attributes from text. See [`Strip`] for which attributes can
1330 /// be stripped.
1331 ///
1332 /// # Examples
1333 ///
1334 /// ```no_run
1335 /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
1336 ///
1337 /// /// Removes colors
1338 /// fn strip_colors(ph: &PluginHandle<'_>, s: &str) -> String {
1339 /// ph.strip(s, Strip::new().colors(true))
1340 /// }
1341 /// ```
1342 #[cfg(not(feature = "nul_in_strip"))]
1343 pub fn strip(&self, s: &str, strip: Strip) -> String {
1344 // make sure s doesn't contain nuls
1345 assert!(!s.as_bytes().contains(&0), "embedded nuls are not allowed");
1346 let mut out = Vec::with_capacity(s.len());
1347 let in_ptr = s.as_ptr() as *const _;
1348 let in_len = s.len().try_into().unwrap();
1349 let flags = strip.flags();
1350 // NOTE: avoid panicking from here.
1351 let stripped = unsafe {
1352 ph_call!(hexchat_strip(self, in_ptr, in_len, flags))
1353 };
1354 out.extend_from_slice(unsafe { CStr::from_ptr(stripped) }.to_bytes());
1355 unsafe {
1356 ph_call!(hexchat_free(self, stripped as *const _));
1357 }
1358 // we can panic again here, tho this should never panic.
1359 String::from_utf8(out).unwrap()
1360 }
1361
1362 /// Sets a pluginpref.
1363 ///
1364 /// Note: If two pluginprefs exist with the same name, but different ASCII
1365 /// case, only one will be available through `pluginpref_get`.
1366 ///
1367 /// # Panics
1368 ///
1369 /// Panics if the plugin is not registered.
1370 ///
1371 /// # Examples
1372 ///
1373 /// ```no_run
1374 /// use hexchat_unsafe_plugin::PluginHandle;
1375 ///
1376 /// fn set_str(ph: &PluginHandle<'_>, val: &str) {
1377 /// ph.pluginpref_set("string", val);
1378 /// }
1379 ///
1380 /// fn set_int(ph: &PluginHandle<'_>, val: i32) {
1381 /// ph.pluginpref_set("int", &format!("{}", val));
1382 /// }
1383 /// ```
1384 pub fn pluginpref_set(
1385 &self,
1386 var: &str,
1387 value: &str,
1388 ) -> Result<(), PluginPrefError> {
1389 assert!(!self.info.name.is_null(), "must register plugin first");
1390 if value.starts_with(' ') {
1391 return Err(PluginPrefError::InvalidValue)
1392 }
1393 let var_len = var.len();
1394 let var = check_pluginpref_var(var)?;
1395 let mut val_len = value.len();
1396 // octal is \000 - \377, adds 3 bytes
1397 val_len += 3 * value.bytes().filter(|&x| x < 32 || x > 127).count();
1398 // special chars are \t \n etc, or octal - 2 bytes
1399 val_len -= 2 * value.bytes().filter(|&x| {
1400 matches!(
1401 x,
1402 | b'\n'
1403 | b'\r'
1404 | b'\t'
1405 | b'\x0C' // \f
1406 | b'\x08' // \b
1407 )
1408 }).count();
1409 // additionally \ and " also get an extra byte
1410 val_len += value.bytes().filter(|&x| x == b'"' || x == b'\\').count();
1411 // 3 bytes from " = ", < 512 because 511 + NUL
1412 if var_len + val_len + 3 < 512 {
1413 let value = CString::new(value).unwrap();
1414 let success = unsafe {
1415 ph_call!(
1416 hexchat_pluginpref_set_str(
1417 self,
1418 var.as_ptr(),
1419 value.as_ptr()
1420 )
1421 )
1422 } != 0;
1423 if !success {
1424 Err(PluginPrefError::Failed)
1425 } else {
1426 Ok(())
1427 }
1428 } else {
1429 Err(PluginPrefError::TooLong)
1430 }
1431 }
1432
1433 /// Retrieves a pluginpref.
1434 ///
1435 /// # Panics
1436 ///
1437 /// Panics if the plugin is not registered.
1438 ///
1439 /// # Examples
1440 ///
1441 /// ```no_run
1442 /// use hexchat_unsafe_plugin::PluginHandle;
1443 ///
1444 /// fn get_str(ph: &PluginHandle<'_>) -> String {
1445 /// ph.pluginpref_get("string").unwrap_or(String::new())
1446 /// }
1447 ///
1448 /// fn get_int(ph: &PluginHandle<'_>) -> i32 {
1449 /// ph.pluginpref_get("int").unwrap_or(String::new()).parse().unwrap_or(-1)
1450 /// }
1451 /// ```
1452 pub fn pluginpref_get(
1453 &self,
1454 var: &str,
1455 ) -> Result<String, PluginPrefError> {
1456 assert!(!self.info.name.is_null(), "must register plugin first");
1457 let var = check_pluginpref_var(var)?;
1458 let mut buffer: [MaybeUninit<c_char>; 512] = unsafe {
1459 MaybeUninit::uninit().assume_init()
1460 };
1461 let success = unsafe {
1462 ph_call!(
1463 hexchat_pluginpref_get_str(
1464 self,
1465 var.as_ptr(),
1466 buffer.as_mut_ptr() as *mut _
1467 )
1468 )
1469 } != 0;
1470 if !success {
1471 Err(PluginPrefError::Failed)
1472 } else {
1473 match unsafe {
1474 CStr::from_ptr(buffer.as_ptr() as *const _)
1475 }.to_owned().into_string() {
1476 Ok(s) => Ok(s),
1477 Err(e) => Err(PluginPrefError::Utf8Error(e)),
1478 }
1479 }
1480 }
1481
1482 /// Removes a pluginpref.
1483 ///
1484 /// # Panics
1485 ///
1486 /// Panics if the plugin is not registered.
1487 ///
1488 /// # Examples
1489 ///
1490 /// ```no_run
1491 /// use hexchat_unsafe_plugin::PluginHandle;
1492 ///
1493 /// fn del_str(ph: &PluginHandle<'_>) {
1494 /// let _ = ph.pluginpref_delete("string");
1495 /// }
1496 ///
1497 /// fn del_int(ph: &PluginHandle<'_>) {
1498 /// let _ = ph.pluginpref_delete("int");
1499 /// }
1500 /// ```
1501 pub fn pluginpref_delete(&self, var: &str) -> Result<(), PluginPrefError> {
1502 assert!(!self.info.name.is_null(), "must register plugin first");
1503 let var = check_pluginpref_var(var)?;
1504 let success = unsafe {
1505 ph_call!(hexchat_pluginpref_delete(self, var.as_ptr()))
1506 } != 0;
1507 if !success {
1508 Err(PluginPrefError::Failed)
1509 } else {
1510 Ok(())
1511 }
1512 }
1513
1514 /// Lists pluginprefs.
1515 ///
1516 /// # Panics
1517 ///
1518 /// Panics if the plugin is not registered.
1519 ///
1520 /// # Examples
1521 ///
1522 /// ```no_run
1523 /// use hexchat_unsafe_plugin::PluginHandle;
1524 ///
1525 /// fn list_prefs(ph: &PluginHandle<'_>) {
1526 /// match ph.pluginpref_list() {
1527 /// Ok(it) => for pref in &it {
1528 /// match pref {
1529 /// Ok(pref) => write!(ph, "{}", pref),
1530 /// _ => (),
1531 /// }
1532 /// },
1533 /// _ => (),
1534 /// }
1535 /// }
1536 /// ```
1537 pub fn pluginpref_list(&self) -> Result<PluginPrefList, PluginPrefError> {
1538 assert!(!self.info.name.is_null(), "must register plugin first");
1539 let mut buffer: [MaybeUninit<c_char>; 4096] = unsafe {
1540 MaybeUninit::uninit().assume_init()
1541 };
1542 let success = unsafe {
1543 ph_call!(
1544 hexchat_pluginpref_list(self, buffer.as_mut_ptr() as *mut _)
1545 )
1546 } != 0;
1547 if !success {
1548 Err(PluginPrefError::Failed)
1549 } else {
1550 let mut list = PluginPrefList {
1551 inner: unsafe {
1552 CStr::from_ptr(buffer.as_ptr() as *const _)
1553 }.to_owned().into_bytes(),
1554 };
1555 list.inner.iter_mut().for_each(|x| if *x == b',' { *x = 0; });
1556 Ok(list)
1557 }
1558 }
1559
1560 // ******* //
1561 // PRIVATE //
1562 // ******* //
1563
1564 fn find_valid_context(&self) -> Option<Context<'ph>> {
1565 unsafe {
1566 let channel_key = cstr(b"channels\0");
1567 let context_key = cstr(b"context\0");
1568 // TODO wrap this in a safer API, with proper Drop
1569 #[allow(unused_mut)]
1570 let mut list = ph_call!(hexchat_list_get(self, channel_key));
1571 // hexchat does this thing where it puts a context in a list_str.
1572 // this *is* the proper way to do this, but it looks weird.
1573 let ctx = if ph_call!(hexchat_list_next(self, list)) != 0 {
1574 Some(ph_call!(hexchat_list_str(self, list, context_key)) as *const internals::HexchatContext)
1575 } else {
1576 None
1577 };
1578 ph_call!(hexchat_list_free(self, list));
1579 ctx.map(|ctx| wrap_context(self, ctx))
1580 }
1581 }
1582
1583 /// Same as `set_context` but it takes `&self`.
1584 fn set_context_internal(&self, ctx: &Context<'ph>) -> bool {
1585 if let Some(ctx) = ctx.ctx.upgrade() {
1586 unsafe {
1587 ph_call!(hexchat_set_context(self, *ctx)) != 0
1588 }
1589 } else {
1590 false
1591 }
1592 }
1593}
1594
1595impl<'a> EventAttrs<'a> {
1596 /// Creates a new `EventAttrs`.
1597 #[allow(deprecated)]
1598 pub fn new() -> EventAttrs<'a> {
1599 EventAttrs {
1600 server_time: None,
1601 raw_server_time: 0,
1602 use_raw_server_time: false,
1603 _dummy: PhantomData,
1604 }
1605 }
1606}
1607
1608impl<'a> From<&'a RawAttrs> for EventAttrs<'a> {
1609 #[allow(deprecated)]
1610 fn from(other: &'a RawAttrs) -> EventAttrs<'a> {
1611 EventAttrs {
1612 server_time: if other.server_time_utc > 0 { Some(UNIX_EPOCH + Duration::from_secs(other.server_time_utc as u64)) } else { None },
1613 raw_server_time: other.server_time_utc,
1614 // Defaults to false for API compatibility.
1615 use_raw_server_time: false,
1616 _dummy: PhantomData,
1617 }
1618 }
1619}
1620
1621impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
1622/*
1623 * These cause UB:
1624 * `hexchat_command` may invalidate the plugin context.
1625 * `hexchat_find_context` and `hexchat_nickcmp` use the plugin context without checking it.
1626 * `hexchat_get_prefs` uses the plugin context if name == "state_cursor" or "id" (or anything with
1627 * the same hash).
1628 * `hexchat_list_get` uses the plugin context if name == "notify" (or anything with the same hash).
1629 * `hexchat_list_str`, `hexchat_list_int`,
1630 * `hexchat_emit_print`, `hexchat_emit_print_attrs` use the plugin context.
1631 * `hexchat_send_modes` uses the plugin context.
1632 * We need to wrap them (or, alternatively, hexchat_command). However, there's no (safe) way to get
1633 * a valid context afterwards.
1634 * - Actually that's a lie. Hexchat does a trick to give you a context as part of the channel list.
1635 * We can use that to our advantage. I'm not sure if it's better to wrap hexchat_command or the
1636 * other functions, tho.
1637 * (Do we want to walk a linked list every time we use hexchat_command? I'd think
1638 * hexchat_command is the most used API function... Plus, emit_print could indirectly
1639 * invalidate the context as well.)
1640 *
1641 * For performance we put them behind an ValidContext - things that don't invalidate the
1642 * context take an `&mut self`, things that do take an `self`.
1643 */
1644
1645 /// Finds an open context for the given servname and channel.
1646 pub fn find_context(&self, servname: Option<&str>, channel: Option<&str>) -> Option<Context<'ph>> {
1647 let ph = &self.ph;
1648 let servname = servname.map(|x| CString::new(x).unwrap());
1649 let channel = channel.map(|x| CString::new(x).unwrap());
1650 let ctx = unsafe {
1651 let sptr = servname.map(|x| x.as_ptr()).unwrap_or(ptr::null());
1652 let cptr = channel.map(|x| x.as_ptr()).unwrap_or(ptr::null());
1653 ph_call!(hexchat_find_context(ph, sptr, cptr))
1654 };
1655 if ctx.is_null() {
1656 None
1657 } else {
1658 Some(unsafe { wrap_context(self.ph, ctx) })
1659 }
1660 }
1661
1662 /// Compares two nicks based on the server's case mapping.
1663 ///
1664 /// ```no_run
1665 /// use hexchat_unsafe_plugin::ValidContext;
1666 ///
1667 /// /// Checks if the two nicks below are equal.
1668 /// fn compare_nicks(context: ValidContext<'_, '_>) -> bool {
1669 /// context.nickcmp("hello", "HELLO").is_eq()
1670 /// }
1671 /// ```
1672 pub fn nickcmp(&self, nick1: &str, nick2: &str) -> std::cmp::Ordering {
1673 use std::cmp::Ordering;
1674 let ph = &self.ph;
1675 // need to put this in a more permanent position than temporaries
1676 let nick1 = CString::new(nick1).unwrap();
1677 let nick2 = CString::new(nick2).unwrap();
1678 let res = unsafe {
1679 ph_call!(hexchat_nickcmp(ph, nick1.as_ptr(), nick2.as_ptr()))
1680 };
1681 if res < 0 {
1682 Ordering::Less
1683 } else if res > 0 {
1684 Ordering::Greater
1685 } else {
1686 Ordering::Equal
1687 }
1688 }
1689
1690 /// Sends a list of modes to the current channel.
1691 pub fn send_modes<'b, I: IntoIterator<Item=&'b str>>(&mut self, iter: I, mpl: i32, sign: char, mode: char) {
1692 let ph = &self.ph;
1693 assert!(sign == '+' || sign == '-', "sign must be + or -");
1694 assert!(mode.is_ascii(), "mode must be ascii");
1695 assert!(mpl >= 0, "mpl must be non-negative");
1696 let v: Vec<CString> = iter.into_iter().map(|s| CString::new(s).unwrap()).collect();
1697 let mut v2: Vec<*const c_char> = (&v).iter().map(|x| x.as_ptr()).collect();
1698 let arr: &mut [*const c_char] = &mut *v2;
1699 unsafe {
1700 ph_call!(hexchat_send_modes(ph, arr.as_mut_ptr(), arr.len() as c_int,
1701 mpl as c_int, sign as c_char, mode as c_char));
1702 }
1703 // some hexchat forks may invalidate the context here.
1704 // just pretend everything is fine and don't bump the major version.
1705 if !self.set_context(&self.get_context()) {
1706 let context = self.ph.find_valid_context().unwrap();
1707 if !self.set_context(&context) { panic!(); }
1708 }
1709 }
1710
1711 /// Returns a client setting.
1712 ///
1713 /// # Examples
1714 ///
1715 /// ```no_run
1716 /// use hexchat_unsafe_plugin::ValidContext;
1717 ///
1718 /// /// Returns the user's configured main nick.
1719 /// fn main_nick(context: ValidContext<'_, '_>) -> String {
1720 /// context.get_prefs("irc_nick1").unwrap().into_string().unwrap()
1721 /// }
1722 /// ```
1723 pub fn get_prefs(&self, name: &str) -> Option<PrefValue> {
1724 let ph = &self.ph;
1725 let name = CString::new(name).unwrap();
1726 let mut string = 0 as *const c_char;
1727 let mut int: c_int = 0;
1728 match unsafe {
1729 ph_call!(hexchat_get_prefs(ph, name.as_ptr(), &mut string, &mut int))
1730 } {
1731 0 => None,
1732 1 => Some(PrefValue::String(unsafe {
1733 CStr::from_ptr(string).to_owned().into_string().unwrap()
1734 })),
1735 2 => Some(PrefValue::Int(int as _)),
1736 3 => Some(PrefValue::Bool(int != 0)),
1737 _ => panic!("unsupported type in get_prefs"),
1738 }
1739 }
1740
1741 /// Executes a command.
1742 ///
1743 /// # Examples
1744 ///
1745 /// ```no_run
1746 /// use hexchat_unsafe_plugin::{ValidContext};
1747 ///
1748 /// fn join(context: ValidContext<'_, '_>, channel: &str) {
1749 /// context.command(&format!("join {}", channel));
1750 /// }
1751 /// ```
1752 pub fn command(self, cmd: &str) {
1753 let ph = self.ph;
1754 // need to put this in a more permanent position than temporaries
1755 let cmd = CString::new(cmd).unwrap();
1756 unsafe {
1757 ph_call!(hexchat_command(ph, cmd.as_ptr()))
1758 }
1759 }
1760
1761 /// Prints an event message, and returns a success status (whether or not
1762 /// the event exists).
1763 ///
1764 /// # Examples
1765 ///
1766 /// ```no_run
1767 /// use hexchat_unsafe_plugin::{ValidContext};
1768 ///
1769 /// fn emit_channel_message(context: ValidContext<'_, '_>, nick: &str, msg: &str) {
1770 /// context.emit_print("Channel Message", [nick, msg]);
1771 /// }
1772 /// ```
1773 pub fn emit_print<'b, I: IntoIterator<Item=&'b str>>(self, event: &str, args: I) -> bool {
1774 let ph = self.ph;
1775 let event = CString::new(event).unwrap();
1776 let mut args_cs: [Option<CString>; 4] = [None, None, None, None];
1777 {
1778 let mut iter = args.into_iter();
1779 for i in 0..4 {
1780 args_cs[i] = iter.next().map(|x| CString::new(x).unwrap());
1781 if args_cs[i].is_none() {
1782 break;
1783 }
1784 }
1785 if iter.next().is_some() {
1786 // it's better to panic so as to get bug reports when we need to increase this
1787 panic!("too many arguments to emit_print (max 4), or iterator not fused");
1788 }
1789 }
1790 let mut argv: [*const c_char; 5] = [ptr::null(); 5];
1791 for i in 0..4 {
1792 argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr());
1793 }
1794 unsafe {
1795 ph_call!(hexchat_emit_print(ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0
1796 }
1797 }
1798
1799 /// Prints an event message, and returns a success status (whether or not
1800 /// the event exists).
1801 ///
1802 /// Also allows setting event attributes.
1803 ///
1804 /// # Examples
1805 ///
1806 /// ```no_run
1807 /// use std::time::SystemTime;
1808 /// use hexchat_unsafe_plugin::{EventAttrs, ValidContext};
1809 ///
1810 /// fn emit_channel_message_at(context: ValidContext<'_, '_>, time: Option<SystemTime>, nick: &str, msg: &str) {
1811 /// let mut attrs = EventAttrs::new();
1812 /// attrs.server_time = time;
1813 /// context.emit_print_attrs(attrs, "Channel Message", [nick, msg]);
1814 /// }
1815 /// ```
1816 pub fn emit_print_attrs<'b, I: IntoIterator<Item=&'b str>>(self, attrs: EventAttrs, event: &str, args: I) -> bool {
1817 let ph = self.ph;
1818 let event = CString::new(event).unwrap();
1819 let mut args_cs: [Option<CString>; 4] = [None, None, None, None];
1820 {
1821 let mut iter = args.into_iter();
1822 for i in 0..4 {
1823 args_cs[i] = iter.next().map(|x| CString::new(x).unwrap());
1824 if args_cs[i].is_none() {
1825 break;
1826 }
1827 }
1828 if let Some(_) = iter.next() {
1829 // it's better to panic so as to get bug reports when we need to increase this
1830 panic!("too many arguments to emit_print_attrs (max 4), or iterator not fused");
1831 }
1832 }
1833 let mut argv: [*const c_char; 5] = [ptr::null(); 5];
1834 for i in 0..4 {
1835 argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr());
1836 }
1837 let helper = unsafe { HexchatEventAttrsHelper::new_with(ph, attrs) };
1838 unsafe {
1839 ph_call!(hexchat_emit_print_attrs(ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0
1840 }
1841 }
1842
1843 /// Retrieves a list.
1844 ///
1845 /// # Examples
1846 ///
1847 /// ```no_run
1848 /// use hexchat_unsafe_plugin::list::Contexts;
1849 /// use hexchat_unsafe_plugin::PluginHandle;
1850 ///
1851 /// fn print_contexts(ph: &mut PluginHandle<'_>) {
1852 /// ph.ensure_valid_context(|ph| {
1853 /// let mut contexts = ph.list(&Contexts);
1854 /// while let Some(context) = contexts.next() {
1855 /// write!(ph, "{}", context.name().unwrap());
1856 /// }
1857 /// })
1858 /// }
1859 /// ```
1860 pub fn list<T: list::List>(&'a self, t: &'a T) -> list::Entries<'a, 'ph, T> {
1861 let ph = &self.ph;
1862 let list = CString::new(&*t.name()).unwrap();
1863 let list = unsafe {
1864 ph_call!(hexchat_list_get(ph, list.as_ptr()))
1865 };
1866 list::Entries {
1867 context: self.ph,
1868 list: list,
1869 t: t,
1870 }
1871 }
1872
1873 // ******** //
1874 // FORWARDS //
1875 // ******** //
1876 // We can't just deref because then you could recursively ensure valid context and then it'd no
1877 // longer work.
1878
1879 /// Returns the current context.
1880 ///
1881 /// See [`PluginHandle::get_context`].
1882 pub fn get_context(&self) -> Context<'ph> {
1883 self.ph.get_context()
1884 }
1885
1886 /// Sets the current context.
1887 ///
1888 /// Returns `true` if the context is valid, `false` otherwise.
1889 ///
1890 /// See [`PluginHandle::set_context`].
1891 // One would think this is unsound, but if the given context isn't valid,
1892 // the current context is unchanged and still valid.
1893 // But if the given context is valid, it becomes the new, valid, current
1894 // context.
1895 // So either way we're still in a valid context when this returns.
1896 pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool {
1897 self.ph.set_context_internal(ctx)
1898 }
1899
1900 /// Prints to the hexchat buffer.
1901 ///
1902 /// See [`PluginHandle::print`].
1903 pub fn print<T: ToString>(&self, s: T) {
1904 self.ph.print(s)
1905 }
1906
1907 /// Prints to the hexchat buffer.
1908 ///
1909 /// Glue for usage of the [`write!`] macro with hexchat.
1910 ///
1911 /// See [`PluginHandle::write_fmt`].
1912 ///
1913 /// # Panics
1914 ///
1915 /// This panics if any broken formatting trait implementations are used in
1916 /// the format arguments. See also [`format!`].
1917 pub fn write_fmt(&self, fmt: fmt::Arguments<'_>) {
1918 self.ph.write_fmt(fmt)
1919 }
1920
1921 /// Sets a command hook.
1922 ///
1923 /// See [`PluginHandle::hook_command`].
1924 pub fn hook_command<'f, F>(&self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
1925 self.ph.hook_command(cmd, pri, help, cb)
1926 }
1927 /// Sets a server hook.
1928 ///
1929 /// See [`PluginHandle::hook_server`].
1930 pub fn hook_server<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
1931 self.ph.hook_server(cmd, pri, cb)
1932 }
1933 /// Sets a server hook with attributes.
1934 ///
1935 /// See [`PluginHandle::hook_server_attrs`].
1936 pub fn hook_server_attrs<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
1937 self.ph.hook_server_attrs(cmd, pri, cb)
1938 }
1939 /// Sets a print hook.
1940 ///
1941 /// See [`PluginHandle::hook_print`].
1942 pub fn hook_print<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
1943 self.ph.hook_print(name, pri, cb)
1944 }
1945 /// Sets a print hook with attributes.
1946 ///
1947 /// See [`PluginHandle::hook_print_attrs`].
1948 pub fn hook_print_attrs<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
1949 self.ph.hook_print_attrs(name, pri, cb)
1950 }
1951 /// Sets a timer hook.
1952 ///
1953 /// See [`PluginHandle::hook_timer`].
1954 pub fn hook_timer<'f, F>(&self, timeout: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'f + RefUnwindSafe, 'f: 'ph {
1955 self.ph.hook_timer(timeout, cb)
1956 }
1957 /// Returns context and client information.
1958 ///
1959 /// See [`PluginHandle::get_info`].
1960 pub fn get_info<'b>(&'b self, id: InfoId) -> Option<Cow<'b, str>> {
1961 self.ph.get_info(id)
1962 }
1963 /// Returns the plugin directory.
1964 ///
1965 /// See [`PluginHandle::get_libdirfs`].
1966 pub fn get_libdirfs(&self) -> Option<CString> {
1967 self.ph.get_libdirfs()
1968 }
1969 /// Strips attributes from text.
1970 ///
1971 /// See [`PluginHandle::strip`].
1972 pub fn strip(&self, s: &str, strip: Strip) -> String {
1973 self.ph.strip(s, strip)
1974 }
1975 /// Sets a pluginpref.
1976 ///
1977 /// See [`PluginHandle::pluginpref_set`].
1978 pub fn pluginpref_set(
1979 &self,
1980 var: &str,
1981 value: &str,
1982 ) -> Result<(), PluginPrefError> {
1983 self.ph.pluginpref_set(var, value)
1984 }
1985 /// Retrieves a pluginpref.
1986 ///
1987 /// See [`PluginHandle::pluginpref_get`].
1988 pub fn pluginpref_get(
1989 &self,
1990 var: &str,
1991 ) -> Result<String, PluginPrefError> {
1992 self.ph.pluginpref_get(var)
1993 }
1994 /// Removes a pluginpref.
1995 ///
1996 /// See [`PluginHandle::pluginpref_delete`].
1997 pub fn pluginpref_delete(&self, var: &str) -> Result<(), PluginPrefError> {
1998 self.ph.pluginpref_delete(var)
1999 }
2000 /// Lists pluginprefs.
2001 ///
2002 /// See [`PluginHandle::pluginpref_list`].
2003 pub fn pluginpref_list(&self) -> Result<PluginPrefList, PluginPrefError> {
2004 self.ph.pluginpref_list()
2005 }
2006}
2007
2008// ******* //
2009// PRIVATE //
2010// ******* //
2011
2012// Type aliases, used for safety type checking.
2013// /// Userdata type used by a command hook.
2014// We actually do want RefUnwindSafe. This function may be called multiple times, and it's not
2015// automatically invalidated if it panics, so it may be called again after it panics. If you need
2016// mutable state, std provides std::sync::Mutex which has poisoning. Other interior mutability with
2017// poisoning could also be used. std doesn't have anything for single-threaded performance (yet),
2018// but hexchat isn't particularly performance-critical.
2019// type CommandHookUd = Box<dyn Fn(Word, WordEol) -> Eat + std::panic::RefUnwindSafe>;
2020// /// Userdata type used by a server hook.
2021// type ServerHookUd = Box<dyn Fn(Word, WordEol, EventAttrs) -> Eat + std::panic::RefUnwindSafe>;
2022// /// Userdata type used by a print hook.
2023// type PrintHookUd = Box<dyn Fn(Word, EventAttrs) -> Eat + std::panic::RefUnwindSafe>;
2024// /// Userdata type used by a timer hook.
2025// type TimerHookUd = Box<dyn Fn() -> bool + std::panic::RefUnwindSafe>;
2026/// Userdata type used by a hook
2027type HookUd<'f> = Box<dyn Fn(*const *const c_char, *const *const c_char, *const RawAttrs) -> Eat + RefUnwindSafe + 'f>;
2028/// Contexts
2029type Contexts = Rc<AssertUnwindSafe<RefCell<HashSet<Rc<*const internals::HexchatContext>>>>>;
2030
2031/// The contents of an empty CStr.
2032///
2033/// This is useful where you need a non-null CStr.
2034// NOTE: MUST BE b"\0"!
2035const EMPTY_CSTRING_DATA: &[u8] = b"\0";
2036
2037/// Converts a nul-terminated const bstring to a C string.
2038///
2039/// # Panics
2040///
2041/// Panics if b contains embedded nuls.
2042// TODO const fn, once that's possible
2043fn cstr(b: &'static [u8]) -> *const c_char {
2044 CStr::from_bytes_with_nul(b).unwrap().as_ptr()
2045}
2046
2047/// Clones an Rc straight from a raw pointer.
2048///
2049/// # Safety
2050///
2051/// This function is unsafe because `ptr` must hame come from `Rc::into_raw`.
2052unsafe fn rc_clone_from_raw<T>(ptr: *const T) -> Rc<T> {
2053 let rc = ManuallyDrop::new(Rc::from_raw(ptr));
2054 Rc::clone(&rc)
2055}
2056
2057/// Converts a **valid** context pointer into a Context (Rust-managed) struct.
2058///
2059/// # Safety
2060///
2061/// This function doesn't validate the context.
2062unsafe fn wrap_context<'ph>(ph: &PluginHandle<'ph>, ctx: *const internals::HexchatContext) -> Context<'ph> {
2063 let contexts = ph.contexts.clone();
2064 if ctx.is_null() {
2065 Context { contexts, ctx: RcWeak::new(), _ph: PhantomData }
2066 } else {
2067 let weak_ctxp = (|| {
2068 // need to drop the borrow(), so use an (|| IIFE)()
2069 contexts.borrow().get(&ctx).map(|x| {
2070 Rc::downgrade(x)
2071 })
2072 })().unwrap_or_else(|| {
2073 let ctxp = Rc::new(ctx);
2074 let weak_ctxp = Rc::downgrade(&ctxp);
2075 contexts.borrow_mut().insert(ctxp);
2076 weak_ctxp
2077 });
2078 Context { contexts, ctx: weak_ctxp, _ph: PhantomData }
2079 }
2080}
2081
2082/// Prints an &str to hexchat, trying to avoid allocations.
2083///
2084/// # Safety
2085///
2086/// This function does not check the passed in argument.
2087///
2088/// # Panics
2089///
2090/// Panics if panic_on_nul is true and the string contains embedded nuls.
2091unsafe fn hexchat_print_str(ph: *mut RawPh, s: &str, panic_on_nul: bool) {
2092 match CString::new(s) {
2093 Result::Ok(cs @ _) => {
2094 let csr: &CStr = &cs;
2095 ((*ph).hexchat_print)(ph, csr.as_ptr())
2096 },
2097 e @ _ => if panic_on_nul {e.unwrap();}, // TODO nul_position?
2098 }
2099}
2100
2101/// Helper to manage owned RawAttrs
2102struct HexchatEventAttrsHelper<'a, 'ph>(*mut RawAttrs, &'a PluginHandle<'ph>) where 'ph: 'a;
2103
2104impl<'a, 'ph> HexchatEventAttrsHelper<'a, 'ph> where 'ph: 'a {
2105 /// Creates a new, empty `HexchatEventAttrsHelper`.
2106 ///
2107 /// # Safety
2108 ///
2109 /// `ph` must be a valid raw plugin handle.
2110 unsafe fn new(ph: &'a PluginHandle<'ph>) -> Self {
2111 HexchatEventAttrsHelper(ph_call!(hexchat_event_attrs_create(ph)), ph)
2112 }
2113
2114 /// Creates a new `HexchatEventAttrsHelper` for a given `EventAttrs`.
2115 ///
2116 /// # Safety
2117 ///
2118 /// `ph` must be a valid raw plugin handle.
2119 unsafe fn new_with(ph: &'a PluginHandle<'ph>, attrs: EventAttrs<'_>) -> Self {
2120 let helper = Self::new(ph);
2121 #[allow(deprecated)]
2122 let v = if attrs.use_raw_server_time {
2123 attrs.raw_server_time
2124 } else {
2125 attrs.server_time.or(Some(UNIX_EPOCH)).map(|st| {
2126 match st.duration_since(UNIX_EPOCH) {
2127 Ok(n) => n.as_secs(),
2128 Err(_) => 0
2129 }
2130 }).filter(|&st| {
2131 st < (time_t::max_value() as u64)
2132 }).unwrap() as time_t
2133 };
2134 (*helper.0).server_time_utc = v;
2135 helper
2136 }
2137}
2138
2139impl<'a, 'ph> Drop for HexchatEventAttrsHelper<'a, 'ph> where 'ph: 'a {
2140 fn drop(&mut self) {
2141 unsafe {
2142 ph_call!(hexchat_event_attrs_free(self.1, self.0))
2143 }
2144 }
2145}
2146
2147/// Plugin data stored in the hexchat plugin_handle.
2148struct PhUserdata<'ph> {
2149 plug: Pin<Box<dyn Plugin<'ph> + 'ph>>,
2150 contexts: Contexts,
2151 // this is never read, but we need to not drop it until we can drop it
2152 _context_hook: HookHandle<'ph, 'ph>,
2153 pluginfo: PluginInfo,
2154}
2155
2156/// Puts the userdata in the plugin handle.
2157///
2158/// # Safety
2159///
2160/// This function is unsafe because it doesn't check if the pointer is valid.
2161///
2162/// Improper use of this function can leak memory.
2163unsafe fn put_userdata<'ph>(ph: LtPhPtr<'ph>, ud: Box<PhUserdata<'ph>>) {
2164 (*ph.ph).userdata = Box::into_raw(ud) as *mut c_void;
2165}
2166
2167/// Pops the userdata from the plugin handle.
2168///
2169/// # Safety
2170///
2171/// This function is unsafe because it doesn't check if the pointer is valid.
2172unsafe fn pop_userdata<'ph>(ph: LtPhPtr<'ph>) -> Box<PhUserdata<'ph>> {
2173 Box::from_raw(mem::replace(&mut (*ph.ph).userdata, ptr::null_mut()) as *mut PhUserdata<'ph>)
2174}
2175
2176fn test_pluginpref_var(var: &[u8]) -> Result<(), PluginPrefError> {
2177 if var.len() >= 127
2178 || var.len() < 1
2179 // rust uses the same definition of ascii whitespace
2180 || var.last().unwrap().is_ascii_whitespace()
2181 || var.contains(&b' ')
2182 || var.contains(&b'=')
2183 || var.contains(&b'\n')
2184 || var.contains(&b',') {
2185 Err(PluginPrefError::InvalidVar)
2186 } else {
2187 Ok(())
2188 }
2189}
2190
2191fn check_pluginpref_var(var: impl Into<Vec<u8>>) -> Result<CString, PluginPrefError> {
2192 let var = var.into();
2193 test_pluginpref_var(&var)?;
2194 Ok(CString::new(var).unwrap())
2195}
2196
2197// *********************** //
2198// PUBLIC OUT OF NECESSITY //
2199// *********************** //
2200
2201#[doc(hidden)]
2202pub unsafe fn hexchat_plugin_init<'ph, T>(plugin_handle: LtPhPtr<'ph>,
2203 plugin_name: *mut *const c_char,
2204 plugin_desc: *mut *const c_char,
2205 plugin_version: *mut *const c_char,
2206 arg: *const c_char) -> c_int
2207 where T: Plugin<'ph> + Default + 'ph {
2208 if plugin_handle.ph.is_null() || plugin_name.is_null() || plugin_desc.is_null() || plugin_version.is_null() {
2209 // we can't really do anything here. just hope this doesn't panic.
2210 eprintln!("hexchat_plugin_init called with a null pointer that shouldn't be null - broken hexchat");
2211 std::process::abort();
2212 }
2213 let ph = plugin_handle.ph as *mut RawPh;
2214 // clear the "userdata" field first thing - if the deinit function gets called (wrong hexchat
2215 // version, other issues), we don't wanna try to drop the hexchat_dummy or hexchat_read_fd
2216 // function as if it were a Box!
2217 (*ph).userdata = ptr::null_mut();
2218 // read the filename so we can pass it on later.
2219 let filename = if !(*plugin_name).is_null() {
2220 if let Ok(fname) = CStr::from_ptr(*plugin_name).to_owned().into_string() {
2221 fname
2222 } else {
2223 hexchat_print_str(ph, "failed to convert filename to utf8 - broken hexchat", false);
2224 return 0;
2225 }
2226 } else {
2227 // no filename specified for some reason, but we can still load
2228 String::new() // empty string
2229 };
2230 // these may be null, unless initialization is successful.
2231 // we set them to null as markers.
2232 *plugin_name = ptr::null();
2233 *plugin_desc = ptr::null();
2234 *plugin_version = ptr::null();
2235 // do some version checks for safety
2236 // NOTE: calling hexchat functions with null plugin_name, plugin_desc, plugin_version is a bit
2237 // dangerous. this particular case is "ok".
2238 {
2239 let ver = ((*ph).hexchat_get_info)(ph, cstr(b"version\0")); // this shouldn't panic
2240 let cstr = CStr::from_ptr(ver);
2241 if let Ok(ver) = cstr.to_str() {
2242 let mut iter = ver.split('.');
2243 let a = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
2244 let b = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
2245 let c = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
2246 // 2.9.6 or greater
2247 if !(a > 2 || (a == 2 && (b > 9 || (b == 9 && (c > 6 || (c == 6)))))) {
2248 return 0;
2249 }
2250 } else {
2251 return 0;
2252 }
2253 }
2254 // we got banned from an IRC network over the following line of code.
2255 hexchat_print_str(ph, "hexchat-unsafe-plugin Copyright (C) 2018, 2021, 2022 Soni L. GPL-3.0-or-later This software is made with love by a queer trans person.", false);
2256 let mut pluginfo = if let Some(pluginfo) = PluginInfo::new(plugin_name, plugin_desc, plugin_version) {
2257 pluginfo
2258 } else {
2259 return 0;
2260 };
2261 let r: thread::Result<Option<Box<_>>> = {
2262 catch_unwind(move || {
2263 // AssertUnwindSafe not Default at the time of writing this
2264 let contexts = Rc::new(AssertUnwindSafe(Default::default()));
2265 let mut pluginhandle = PluginHandle::new(plugin_handle, pluginfo, contexts);
2266 let contexts = Rc::clone(&pluginhandle.contexts);
2267 // must register this before the plugin registers anything else!
2268 let context_hook = pluginhandle.hook_print("Close Context", c_int::min_value(), move |ph, _| {
2269 // just remove the context! it's that simple!
2270 let ctx = ph_call!(hexchat_get_context(ph));
2271 contexts.borrow_mut().remove(&ctx);
2272 EAT_NONE
2273 });
2274 let contexts = Rc::clone(&pluginhandle.contexts);
2275 let mut plug = Box::pin(T::default());
2276 if plug.as_mut().init(&mut pluginhandle, &filename, if !arg.is_null() { Some(CStr::from_ptr(arg).to_str().expect("arg not valid utf-8 - broken hexchat")) } else { None }) {
2277 if !(pluginfo.name.is_null() || pluginfo.desc.is_null() || pluginfo.vers.is_null()) {
2278 Some(Box::new(PhUserdata { plug, pluginfo, contexts, _context_hook: context_hook }))
2279 } else {
2280 // TODO log: forgot to call register
2281 None
2282 }
2283 } else {
2284 if !(pluginfo.name.is_null() || pluginfo.desc.is_null() || pluginfo.vers.is_null()) {
2285 pluginfo.drop_info()
2286 }
2287 None
2288 }
2289 })
2290 };
2291 match r {
2292 Result::Ok(Option::Some(plug @ _)) => {
2293 put_userdata(plugin_handle, plug);
2294 1
2295 },
2296 r @ _ => {
2297 if let Err(e) = r {
2298 log_panic(ph, e);
2299 }
2300 0
2301 },
2302 }
2303}
2304
2305#[doc(hidden)]
2306pub unsafe fn hexchat_plugin_deinit<'ph, T>(plugin_handle: LtPhPtr<'ph>) -> c_int where T: Plugin<'ph> {
2307 let mut safe_to_unload = 1;
2308 // plugin_handle should never be null, but just in case.
2309 if !plugin_handle.ph.is_null() {
2310 let ph = plugin_handle.ph as *mut RawPh;
2311 // userdata should also never be null.
2312 if !(*ph).userdata.is_null() {
2313 {
2314 let mut info: Option<PluginInfo> = None;
2315 {
2316 let mut ausinfo = AssertUnwindSafe(&mut info);
2317 safe_to_unload = match catch_unwind(move || {
2318 let mut userdata = pop_userdata(plugin_handle);
2319 let pluginfo = userdata.pluginfo;
2320 if let Err(e) = catch_unwind(AssertUnwindSafe(|| {
2321 userdata.plug.as_mut().deinit(&mut {
2322 PluginHandle::new(
2323 plugin_handle,
2324 pluginfo,
2325 Rc::clone(&userdata.contexts)
2326 )
2327 });
2328 })) {
2329 // panics in deinit may be retried.
2330 // however, one may need to close hexchat if that
2331 // happens.
2332 put_userdata(plugin_handle, userdata);
2333 std::panic::resume_unwind(e);
2334 }
2335 **ausinfo = Some(pluginfo);
2336 drop(userdata);
2337 }) {
2338 Ok(_) => 1,
2339 Err(e) => {
2340 log_panic(ph, e);
2341 0
2342 }
2343 };
2344 }
2345 if let Some(mut info) = info {
2346 info.drop_info();
2347 safe_to_unload = 1;
2348 }
2349 }
2350 } else {
2351 hexchat_print_str(ph, "plugin userdata was null, broken hexchat-unsafe-plugin?", false);
2352 }
2353 } else {
2354 // we are once again hoping for the best here.
2355 eprintln!("hexchat_plugin_deinit called with a null plugin_handle - broken hexchat");
2356 std::process::abort();
2357 }
2358 safe_to_unload
2359}
2360
2361/// Exports a hexchat plugin.
2362#[macro_export]
2363macro_rules! hexchat_plugin {
2364 ($l:lifetime, $t:ty) => {
2365 #[no_mangle]
2366 pub unsafe extern "C" fn hexchat_plugin_init<$l>(plugin_handle: $crate::LtPhPtr<$l>,
2367 plugin_name: *mut *const $crate::c_char,
2368 plugin_desc: *mut *const $crate::c_char,
2369 plugin_version: *mut *const $crate::c_char,
2370 arg: *const $crate::c_char) -> $crate::c_int {
2371 $crate::hexchat_plugin_init::<$l, $t>(plugin_handle, plugin_name, plugin_desc, plugin_version, arg)
2372 }
2373 #[no_mangle]
2374 pub unsafe extern "C" fn hexchat_plugin_deinit<$l>(plugin_handle: $crate::LtPhPtr<$l>) -> $crate::c_int {
2375 $crate::hexchat_plugin_deinit::<$l, $t>(plugin_handle)
2376 }
2377 // unlike what the documentation states, there's no need to define hexchat_plugin_get_info.
2378 // so we don't. it'd be impossible to make it work well with rust anyway.
2379 };
2380}