Skip to main content

pmpx_plugin/
shell.rs

1//! The plugin side of the ABI: everything between `export!`'s shims and the trait a plugin writes.
2//!
3//! This is the only module in the crate that touches raw memory. It owns three jobs:
4//!
5//! - **Answer the host's questions** through the accessors (`dispatch` is what `export!` points the
6//!   `command` capability at),
7//! - **Build the typed [`Context`]** out of them, so a plugin never sees a key or a pointer,
8//! - **Hand the answer back** in the ABI's shape, with the memory this side owns.
9//!
10//! Everything here runs *inside* the plugin, on data the host lent it. The rules it obeys:
11//!
12//! - Nothing lent may be kept past the call.
13//! - Memory is freed by the side that allocated it: what this module leases out for the answer is
14//!   reclaimed by `free_command` / `free_str`, never by the host.
15//! - A panic must not cross: the shims in `export!` wrap every entry point in `catch_unwind`.
16
17use std::ffi::{OsStr, OsString};
18
19use pmpx_plugin_abi::{
20    PmpxCommand, PmpxContext, PmpxStr, PMPX_ERR_INTERNAL, PMPX_ERR_INVALID_ARGS,
21    PMPX_ERR_UNSUPPORTED_VERB, PMPX_MAX_ITEMS, PMPX_OK,
22};
23
24use crate::{CommandSpec, Context, PackageManager, Verb};
25
26/// What `name` / `family` answer when the plugin panicked before it could answer at all.
27///
28/// A marker rather than an empty string: the host refuses to load a plugin whose name disagrees with
29/// its manifest, and this makes that refusal say what happened instead of showing an empty pair of
30/// quotes.
31pub const PANIC_MARKER: &str = "<the plugin panicked>";
32
33/// Write a [`CommandSpec`] in its cross-boundary form, with the memory allocated by this side.
34///
35/// # Safety
36/// `out` must point at writable memory for a [`PmpxCommand`].
37pub unsafe fn write_command(out: *mut PmpxCommand, spec: CommandSpec) {
38    let program = leak_bytes(&os_to_bytes(&spec.program));
39
40    let args: Vec<PmpxStr> = spec
41        .args
42        .iter()
43        .map(|arg| leak_bytes(&os_to_bytes(arg)))
44        .collect();
45    let args = args.into_boxed_slice();
46    let args_len = args.len();
47    let args = Box::into_raw(args) as *const PmpxStr;
48
49    let cwd = match &spec.cwd {
50        Some(dir) => leak_bytes(&os_to_bytes(dir.as_os_str())),
51        None => PmpxStr::EMPTY,
52    };
53
54    // SAFETY: the caller promises writable memory; every pointer above belongs to this side and is
55    // reclaimed by `free_command`.
56    unsafe {
57        (*out).size = std::mem::size_of::<PmpxCommand>();
58        (*out).program = program;
59        (*out).args = pmpx_plugin_abi::PmpxSlice::new(args, args_len);
60        (*out).cwd = cwd;
61    }
62}
63
64/// Release everything [`write_command`] allocated, without touching the struct itself (it lives on
65/// the host's stack).
66///
67/// # Safety
68/// `command` must come from one successful [`write_command`] on this side, and must be released
69/// once.
70pub unsafe fn free_command(command: *mut PmpxCommand) {
71    if command.is_null() {
72        return;
73    }
74
75    // SAFETY: the caller vouches for the pointer.
76    let command = unsafe { &*command };
77
78    free_str(command.program);
79    free_str(command.cwd);
80
81    if !command.args.is_absent() {
82        // SAFETY: `write_command` leaked exactly this box.
83        let args = unsafe {
84            Box::from_raw(std::ptr::slice_from_raw_parts_mut(
85                command.args.ptr as *mut PmpxStr,
86                command.args.len,
87            ))
88        };
89        for arg in args.iter() {
90            free_str(*arg);
91        }
92    }
93}
94
95/// Give one string back to this side's allocator.
96///
97/// # Safety
98/// `s` must come from [`leak_bytes`] or [`leak_str`], and must be released once.
99pub unsafe fn free_str(s: PmpxStr) {
100    if s.ptr.is_null() {
101        return;
102    }
103    // SAFETY: the caller vouches for the provenance and the single release.
104    let bytes =
105        unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(s.ptr as *mut u8, s.len)) };
106    drop(bytes);
107}
108
109/// Lease out bytes for the host to read.
110pub fn leak_bytes(bytes: &[u8]) -> PmpxStr {
111    let boxed = bytes.to_vec().into_boxed_slice();
112    let len = boxed.len();
113    PmpxStr::new(Box::into_raw(boxed) as *const u8, len)
114}
115
116/// Lease out text for the host to read.
117pub fn leak_str(s: &str) -> PmpxStr {
118    leak_bytes(s.as_bytes())
119}
120
121/// Read a borrowed C string as text, checking UTF-8.
122///
123/// # Safety
124/// `s` must be valid for the duration of the call.
125pub unsafe fn read_str(s: PmpxStr) -> Option<&'static str> {
126    // SAFETY: the caller vouches for the memory; the lifetime is deliberately tied to the call site
127    // by making the caller keep the value inside it.
128    let bytes = unsafe { s.as_bytes() }?;
129    std::str::from_utf8(bytes).ok().map(|text| {
130        // The bytes belong to the host and are valid for this call; saying `'static` here is what
131        // lets the shells read a key without copying it. Every caller uses the value immediately,
132        // inside the same call.
133        unsafe { std::mem::transmute::<&str, &'static str>(text) }
134    })
135}
136
137/// Read a borrowed C string as an owned `OsString`, losslessly where the platform allows it.
138///
139/// # Safety
140/// As [`read_str`].
141pub unsafe fn read_os(s: PmpxStr) -> OsString {
142    // SAFETY: the caller vouches for the memory.
143    let bytes = unsafe { s.as_bytes() }.unwrap_or(&[]);
144    pmpx_plugin_abi::bytes_to_os(bytes)
145}
146
147/// The plugin-side entry point `export!` exposes as the `command` capability.
148///
149/// # Safety
150/// See the safety notes of the ABI's `command` table: the context must be the host's, valid and
151/// read-only for this call, and `out` must be writable.
152pub unsafe fn dispatch(
153    plugin: &dyn PackageManager,
154    context: *const PmpxContext,
155    out: *mut PmpxCommand,
156) -> u32 {
157    if out.is_null() || context.is_null() {
158        return PMPX_ERR_INVALID_ARGS;
159    }
160
161    // SAFETY: non-null, and the host keeps it read-only for the call.
162    let raw = unsafe { &*context };
163
164    // The host says how much of the struct it built; reading past that would be reading whatever
165    // happens to be there.
166    if raw.size < std::mem::size_of::<PmpxContext>() {
167        return PMPX_ERR_INVALID_ARGS;
168    }
169
170    // A verb this build does not know is answered as *unsupported*, not as invalid arguments: that
171    // is what keeps a new verb additive, because it leaves the host's degradation path open.
172    let Some(verb) = Verb::from_abi(raw.verb) else {
173        return PMPX_ERR_UNSUPPORTED_VERB;
174    };
175
176    let Ok(args) = (unsafe { read_args(context) }) else {
177        return PMPX_ERR_INVALID_ARGS;
178    };
179
180    // SAFETY: the context is the host's, valid for this call; the typed context borrows it only for
181    // as long as this function runs.
182    let context = unsafe { Context::from_host(context) };
183
184    // Bracketed so that anything the plugin logs can say what it was working with, and so that a
185    // plugin logging outside a call finds nothing rather than a stale one.
186    let description = context.describe(verb, args.len());
187    let answer = crate::debug::with_call(description, || plugin.command(&context, verb, &args));
188
189    match answer {
190        Ok(spec) => {
191            // SAFETY: `out` was checked non-null, and the host promises it is writable.
192            unsafe { write_command(out, spec) };
193            PMPX_OK
194        }
195        Err(error) => error.code(),
196    }
197}
198
199/// Read the arguments the host sent.
200///
201/// # Safety
202/// `context` must be a valid host context.
203unsafe fn read_args(context: *const PmpxContext) -> Result<Vec<OsString>, ()> {
204    let raw = unsafe { &*context };
205    let key = PmpxStr::new(
206        pmpx_plugin_abi::PMPX_KEY_ARGS.as_ptr(),
207        pmpx_plugin_abi::PMPX_KEY_ARGS.len(),
208    );
209
210    let count = unsafe { (raw.count)(context, key) };
211    // A host that answers an absurd count is refused rather than allocated for.
212    if count > PMPX_MAX_ITEMS {
213        return Err(());
214    }
215
216    let mut args = Vec::with_capacity(count);
217    for index in 0..count {
218        let value = unsafe { (raw.get)(context, key, index) };
219        if value.is_absent() {
220            return Err(());
221        }
222        args.push(unsafe { read_os(value) });
223    }
224    Ok(args)
225}
226
227/// The `free_str` shim's implementation.
228///
229/// # Safety
230/// As [`free_str`].
231pub unsafe fn dispatch_free_str(s: PmpxStr) {
232    // SAFETY: the caller (the host) only passes back what this side leased out.
233    unsafe { free_str(s) };
234}
235
236/// The `free_command` shim's implementation.
237///
238/// # Safety
239/// As [`free_command`].
240pub unsafe fn dispatch_free_command(command: *mut PmpxCommand) {
241    // SAFETY: as above.
242    unsafe { free_command(command) };
243}
244
245/// Wrap one cross-boundary call in `catch_unwind`.
246///
247/// Since Rust 1.81, letting a panic cross an `extern "C"` boundary aborts the process outright, and
248/// the host cannot help at all -- so the plugin has to catch it itself, here.
249pub fn guard(f: impl FnOnce() -> u32) -> u32 {
250    // `AssertUnwindSafe`: once the caller sees an error code it aborts the operation and never looks
251    // at the borrowed state again, so there is nothing for the unwind-safety check to protect.
252    std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(PMPX_ERR_INTERNAL)
253}
254
255/// The same, for one of the two `-> PmpxStr` shims.
256pub fn guard_str(f: impl FnOnce() -> PmpxStr) -> PmpxStr {
257    std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|_| {
258        let marker = leak_str(PANIC_MARKER);
259        // The host frees what it is handed, including this marker.
260        marker
261    })
262}
263
264/// Read the arguments a command table was given, converting paths and arguments both ways.
265///
266/// Re-exported shape helpers: they exist so the shells and the tests do not each write their own
267/// platform `cfg`.
268pub(crate) fn os_to_bytes(s: &OsStr) -> Vec<u8> {
269    pmpx_plugin_abi::os_to_bytes(s)
270}
271
272/// The rustc version this crate was built with, injected by `build.rs`. Diagnostics only.
273pub const BUILD_RUSTC: &str = env!("PMPX_BUILD_RUSTC");
274
275/// The target this crate was built for, injected by `build.rs`. Diagnostics only.
276pub const BUILD_TARGET: &str = env!("PMPX_BUILD_TARGET");
277
278/// Remember the plugin's own name, for the no-host fallback.
279pub fn remember_name(name: &str) {
280    crate::debug::remember_name(name);
281}
282
283/// Install the host's logging table, after the caller has checked its size.
284///
285/// # Safety
286/// As [`debug`](mod@crate::debug)'s own note: the table must be the host's, large enough, and alive for the
287/// process.
288pub unsafe fn install_log_table(table: *const pmpx_plugin_abi::PmpxLog) {
289    // SAFETY: the caller checked the size and the lifetime.
290    unsafe { crate::debug::set_log_table(table) };
291}