Skip to main content

ove/
shell.rs

1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Interactive shell subsystem for oveRTOS.
8//!
9//! Provides [`init`], [`process_char`], and [`register_cmd`] for building a
10//! simple command-line interface over a serial console. Commands are registered
11//! as safe Rust `fn(args: &[&[u8]])` callbacks; the FFI trampoline handles the
12//! C/Rust boundary automatically.
13
14use crate::bindings;
15use crate::error::{Error, Result};
16
17// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
18// calls below): any handle passed to the C API is non-null and refers to a
19// live RTOS object — wrapper constructors establish validity via
20// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
21// a handle is released. Pointer and slice arguments reference caller-owned
22// memory valid for the duration of the call; the C side copies whatever it
23// retains and does not alias them past return (verified against the
24// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
25// pointer casts from user data, slice reconstruction via `from_raw_parts`,
26// or storing a callback across the FFI boundary — carry their own
27// `// SAFETY:` comment.
28
29/// Shell command handler signature.
30///
31/// `args` contains the parsed arguments as byte slices (argv\[0\] is the command name).
32pub type CmdFn = fn(args: &[&[u8]]);
33
34const MAX_CMDS: usize = 16;
35const MAX_ARGS: usize = 8;
36
37struct CmdEntry {
38    name: &'static [u8],
39    handler: CmdFn,
40}
41
42static mut CMD_TABLE: [Option<CmdEntry>; MAX_CMDS] = {
43    const NONE: Option<CmdEntry> = None;
44    [NONE; MAX_CMDS]
45};
46static mut CMD_COUNT: usize = 0;
47
48/// Initialize the shell subsystem.
49pub fn init() -> Result<()> {
50    let rc = unsafe { bindings::ove_shell_init() };
51    Error::from_code(rc)
52}
53
54/// Process a single input character through the shell.
55pub fn process_char(c: i32) {
56    unsafe { bindings::ove_shell_process_char(c) }
57}
58
59/// Register a shell command with a safe Rust handler.
60///
61/// `name` and `help` must be `\0`-terminated `&'static` byte slices.
62/// A single trampoline dispatches to the correct handler by matching `argv[0]`.
63///
64/// # Errors
65/// Returns [`Error::NoMemory`] when the internal command table is full
66/// (capacity is 16 commands).
67pub fn register_cmd(name: &'static [u8], help: &'static [u8], handler: CmdFn) -> Result<()> {
68    let idx = unsafe { CMD_COUNT };
69    if idx >= MAX_CMDS {
70        return Err(Error::NoMemory);
71    }
72
73    unsafe {
74        CMD_TABLE[idx] = Some(CmdEntry { name, handler });
75        CMD_COUNT = idx + 1;
76    }
77
78    let cmd = bindings::ove_shell_cmd {
79        name: name.as_ptr() as *const _,
80        help: help.as_ptr() as *const _,
81        handler: Some(trampoline),
82    };
83    let rc = unsafe { bindings::ove_shell_register_cmd(&cmd) };
84    Error::from_code(rc)
85}
86
87unsafe extern "C" fn trampoline(argc: core::ffi::c_int, argv: *mut *const core::ffi::c_char) {
88    if argc <= 0 || argv.is_null() {
89        return;
90    }
91
92    // Build safe arg slices on the stack
93    let argc = (argc as usize).min(MAX_ARGS);
94    let mut args: [&[u8]; MAX_ARGS] = [&[]; MAX_ARGS];
95    for (i, arg) in args.iter_mut().take(argc).enumerate() {
96        let ptr = unsafe { *argv.add(i) } as *const u8;
97        if !ptr.is_null() {
98            *arg = unsafe { cstr_ptr_to_slice(ptr) };
99        }
100    }
101    let args = &args[..argc];
102
103    // Match argv[0] against registered command names
104    if args.is_empty() {
105        return;
106    }
107    let cmd_name = args[0];
108
109    let count = unsafe { CMD_COUNT };
110    for entry in unsafe { &CMD_TABLE[..count] }.iter().flatten() {
111        // Compare without trailing \0
112        let entry_name = strip_nul(entry.name);
113        if cmd_name == entry_name {
114            (entry.handler)(args);
115            return;
116        }
117    }
118}
119
120fn strip_nul(s: &[u8]) -> &[u8] {
121    if s.last() == Some(&0) {
122        &s[..s.len() - 1]
123    } else {
124        s
125    }
126}
127
128unsafe fn cstr_ptr_to_slice<'a>(ptr: *const u8) -> &'a [u8] {
129    let mut len = 0;
130    // SAFETY: caller (the `trampoline`) guarantees `ptr` is a non-null,
131    // NUL-terminated C string from `argv`; the scan stops at the NUL, and the
132    // resulting slice covers exactly the bytes before it.
133    while unsafe { *ptr.add(len) } != 0 {
134        len += 1;
135    }
136    unsafe { core::slice::from_raw_parts(ptr, len) }
137}
138
139/// Process a complete input line through the shell.
140///
141/// `line` must be a null-terminated byte string (e.g. `b"help\0"`).
142pub fn process_line(line: &[u8]) {
143    unsafe { bindings::ove_shell_process_line(line.as_ptr() as *const _) }
144}
145
146/// Set a hook to capture shell output.
147///
148/// Pass `None` to remove a previously set hook.
149pub fn set_output_hook(hook: bindings::ove_shell_output_hook_t) {
150    unsafe { bindings::ove_shell_set_output_hook(hook) }
151}