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}