1#![no_std]
8#![allow(clippy::missing_safety_doc)]
10#![allow(unused_crate_dependencies)]
11#![cfg(console_enabled)]
12
13use console_rust::{CMD_AVAIL_ALWAYS, CMD_AVAIL_NORMAL, CmdArgs, static_command};
14use core::ffi::c_int;
15use core::sync::atomic::{AtomicI32, Ordering};
16use zx_status::Status;
17
18macro_rules! zx_status {
19 ($status:expr) => {
20 $status.into_raw()
21 };
22}
23
24unsafe extern "C" {
26 static __start_commands: console_rust::Cmd;
27 static __stop_commands: console_rust::Cmd;
28 fn rust_console_match_command(
29 name: *const core::ffi::c_char,
30 mask: u8,
31 ) -> *const console_rust::Cmd;
32 fn rust_console_get_echo() -> bool;
33 fn rust_console_get_exit() -> bool;
34 fn rust_console_set_exit(val: bool);
35 fn console_run_script_locked(string: *const core::ffi::c_char) -> c_int;
36}
37
38#[repr(C)]
39#[derive(Clone, Copy)]
40struct FILE {
41 write: unsafe extern "C" fn(
42 *mut core::ffi::c_void,
43 *const core::ffi::c_char,
44 usize,
45 ) -> core::ffi::c_int,
46 ptr: *mut core::ffi::c_void,
47}
48
49unsafe extern "C" {
50 static mut gStdout: FILE;
51}
52
53struct CapturerState {
54 buf: *mut u8,
55 buf_size: usize,
56 len: usize,
57 original_stdout: Option<FILE>,
58}
59
60struct SyncUnsafeCell<T>(core::cell::UnsafeCell<T>);
61
62unsafe impl<T> Sync for SyncUnsafeCell<T> {}
64
65static CAPTURER_STATE: SyncUnsafeCell<CapturerState> =
66 SyncUnsafeCell(core::cell::UnsafeCell::new(CapturerState {
67 buf: core::ptr::null_mut(),
68 buf_size: 0,
69 len: 0,
70 original_stdout: None,
71 }));
72
73unsafe extern "C" fn write_callback(
74 _ptr: *mut core::ffi::c_void,
75 str_ptr: *const core::ffi::c_char,
76 len: usize,
77) -> core::ffi::c_int {
78 let state = unsafe { &mut *CAPTURER_STATE.0.get() };
79 if state.buf.is_null() || state.buf_size == 0 {
80 return 0;
81 }
82 let to_copy = core::cmp::min(len, state.buf_size - 1 - state.len);
83 if to_copy > 0 {
84 unsafe {
85 core::ptr::copy_nonoverlapping(str_ptr as *const u8, state.buf.add(state.len), to_copy);
86 }
87 state.len += to_copy;
88 unsafe { *state.buf.add(state.len) = 0 };
89 }
90 len as core::ffi::c_int
91}
92
93unsafe fn test_capture_stdout_start(buf: *mut u8, size: usize) {
94 let state = unsafe { &mut *CAPTURER_STATE.0.get() };
95 state.buf = buf;
96 state.buf_size = size;
97 state.len = 0;
98 unsafe { *state.buf = 0 };
99 unsafe {
100 state.original_stdout = Some(gStdout);
101 gStdout = FILE { write: write_callback, ptr: core::ptr::null_mut() };
102 }
103}
104
105unsafe fn test_capture_stdout_stop() {
106 let state = unsafe { &mut *CAPTURER_STATE.0.get() };
107 if let Some(orig) = state.original_stdout {
108 unsafe {
109 gStdout = orig;
110 }
111 state.original_stdout = None;
112 }
113 state.buf = core::ptr::null_mut();
114 state.buf_size = 0;
115 state.len = 0;
116}
117
118static_command!(
120 TEST_CMD,
121 c"mock_success".as_ptr(),
122 c"mock_success help".as_ptr(),
123 mock_success_callback,
124 CMD_AVAIL_NORMAL
125);
126
127static_command!(
128 MOCK_FAILURE,
129 c"mock_failure".as_ptr(),
130 core::ptr::null(),
131 mock_failure_callback,
132 CMD_AVAIL_NORMAL
133);
134
135static_command!(
136 CMD_MOCK_ALWAYS,
137 c"mock_avail_always".as_ptr(),
138 c"mock_avail_always help".as_ptr(),
139 mock_success_callback,
140 CMD_AVAIL_ALWAYS
141);
142
143static MOCK_CALL_COUNT: AtomicI32 = AtomicI32::new(0);
144
145unsafe extern "C" fn mock_success_callback(
146 _argc: c_int,
147 _argv: *const CmdArgs,
148 _flags: u32,
149) -> c_int {
150 MOCK_CALL_COUNT.fetch_add(1, Ordering::Relaxed);
151 zx_status!(Status::OK)
152}
153
154unsafe extern "C" fn mock_failure_callback(
155 _argc: c_int,
156 _argv: *const CmdArgs,
157 _flags: u32,
158) -> c_int {
159 MOCK_CALL_COUNT.fetch_add(1, Ordering::Relaxed);
160 zx_status!(Status::INVALID_ARGS)
161}
162
163fn contains_command_help(output: &[u8], name: &[u8], help: &[u8]) -> bool {
164 output
165 .split(|&b| b == b'\n')
166 .any(|line| line.starts_with(b"\t") && line[1..].starts_with(name) && line.ends_with(help))
167}
168
169fn find_command_index(output: &[u8], name: &[u8]) -> Option<usize> {
170 for (idx, line) in output.split(|&b| b == b'\n').enumerate() {
171 if line.starts_with(b"\t") && line[1..].starts_with(name) {
172 return Some(idx);
173 }
174 }
175 None
176}
177
178#[cfg(ktest)]
180#[unittest::test_suite(name = "console_rust")]
181mod console_tests {
182 use unittest::{
183 assert_eq, assert_nonnull, expect_eq, expect_false, expect_lt, expect_ne, expect_true,
184 };
185
186 #[test]
188 fn console_abi_test() {
189 assert_eq!(core::mem::size_of::<console_rust::CmdArgs>(), 40);
190 assert_eq!(core::mem::align_of::<console_rust::CmdArgs>(), 8);
191 assert_eq!(core::mem::size_of::<console_rust::Cmd>(), 32);
192 assert_eq!(core::mem::align_of::<console_rust::Cmd>(), 8);
193 }
194
195 #[test]
197 fn command_echo_test() {
198 let original_echo = unsafe { rust_console_get_echo() };
199
200 let res = unsafe { console_run_script_locked(c"echo false".as_ptr()) };
202 expect_eq!(res, zx_status!(Status::OK));
203 expect_false!(unsafe { rust_console_get_echo() });
204
205 let res = unsafe { console_run_script_locked(c"echo true".as_ptr()) };
207 expect_eq!(res, zx_status!(Status::OK));
208 expect_true!(unsafe { rust_console_get_echo() });
209
210 let cmd = if original_echo { c"echo true" } else { c"echo false" };
212 unsafe {
213 console_run_script_locked(cmd.as_ptr());
214 }
215 }
216
217 #[test]
219 fn command_exit_test() {
220 let original_exit = unsafe { rust_console_get_exit() };
221
222 unsafe {
223 rust_console_set_exit(false);
224 }
225 let res = unsafe { console_run_script_locked(c"exit".as_ptr()) };
226 expect_eq!(res, zx_status!(Status::OK));
227 expect_false!(unsafe { rust_console_get_exit() });
228
229 unsafe {
231 rust_console_set_exit(original_exit);
232 }
233 }
234
235 #[test]
237 fn command_boot_test_success_test() {
238 unsafe {
240 console_run_script_locked(c"mock_success".as_ptr());
241 }
242 let res = unsafe { console_run_script_locked(c"boot-test-success".as_ptr()) };
243 expect_eq!(res, zx_status!(Status::OK));
244
245 let some_failure = unsafe { console_run_script_locked(c"mock_failure".as_ptr()) };
247 let res = unsafe { console_run_script_locked(c"boot-test-success".as_ptr()) };
248 expect_eq!(res, some_failure);
249
250 unsafe {
252 console_run_script_locked(c"mock_success".as_ptr());
253 }
254 }
255
256 #[test]
258 fn command_and_test() {
259 let some_failure = unsafe { console_run_script_locked(c"mock_failure".as_ptr()) };
261 let res = unsafe { console_run_script_locked(c"and mock_success".as_ptr()) };
262 expect_eq!(res, some_failure);
263
264 unsafe {
266 console_run_script_locked(c"mock_success".as_ptr());
267 }
268 let res = unsafe { console_run_script_locked(c"and mock_success".as_ptr()) };
269 expect_eq!(res, zx_status!(Status::OK));
270 }
271
272 #[test]
274 fn command_repeat_test() {
275 MOCK_CALL_COUNT.store(0, Ordering::Relaxed);
276
277 let res = unsafe { console_run_script_locked(c"repeat 3 mock_success".as_ptr()) };
279 expect_eq!(res, zx_status!(Status::OK));
280 expect_eq!(MOCK_CALL_COUNT.load(Ordering::Relaxed), 3);
281
282 MOCK_CALL_COUNT.store(0, Ordering::Relaxed);
283
284 let res = unsafe { console_run_script_locked(c"repeat 3 mock_failure".as_ptr()) };
286 expect_ne!(res, zx_status!(Status::OK));
287 expect_eq!(MOCK_CALL_COUNT.load(Ordering::Relaxed), 1);
288 }
289
290 #[test]
292 fn command_help_normal_test() {
293 let cmd_ptr = unsafe { rust_console_match_command(c"help".as_ptr(), 0xff) };
294 assert_nonnull!(cmd_ptr);
295 let cb = unsafe { (*cmd_ptr).cmd_callback };
296
297 static BUF: SyncUnsafeCell<[u8; 4096]> =
299 SyncUnsafeCell(core::cell::UnsafeCell::new([0; 4096]));
300 let buf_ptr = BUF.0.get() as *mut u8;
301
302 unsafe {
304 core::ptr::write_bytes(buf_ptr, 0, 4096);
305 test_capture_stdout_start(buf_ptr, 4096);
306 }
307 let res = unsafe { cb(1, core::ptr::null(), 0) };
309 unsafe {
310 test_capture_stdout_stop();
311 }
312
313 expect_eq!(res, zx_status!(Status::OK));
314
315 let output = unsafe { core::slice::from_raw_parts(buf_ptr, 4096) };
317 let len = output.iter().position(|&b| b == b'\0').unwrap_or(output.len());
318 let output = &output[..len];
319
320 expect_true!(contains_command_help(
321 output,
322 b"mock_avail_always",
323 b"mock_avail_always help"
324 ));
325 expect_true!(contains_command_help(output, b"mock_success", b"mock_success help"));
326
327 let always_idx = find_command_index(output, b"mock_avail_always");
329 let success_idx = find_command_index(output, b"mock_success");
330
331 match (always_idx, success_idx) {
332 (Some(a), Some(s)) => {
333 expect_lt!(a, s);
334 }
335 _ => {
336 record_failure!();
337 }
338 }
339 }
340
341 #[test]
343 fn command_help_panic_test() {
344 let cmd_ptr = unsafe { rust_console_match_command(c"help".as_ptr(), 0xff) };
345 assert_nonnull!(cmd_ptr);
346 let cb = unsafe { (*cmd_ptr).cmd_callback };
347
348 static BUF: SyncUnsafeCell<[u8; 4096]> =
350 SyncUnsafeCell(core::cell::UnsafeCell::new([0; 4096]));
351 let buf_ptr = BUF.0.get() as *mut u8;
352
353 unsafe {
355 core::ptr::write_bytes(buf_ptr, 0, 4096);
356 test_capture_stdout_start(buf_ptr, 4096);
357 }
358 let res = unsafe { cb(1, core::ptr::null(), console_rust::CMD_FLAG_PANIC) };
360 unsafe {
361 test_capture_stdout_stop();
362 }
363
364 expect_eq!(res, zx_status!(Status::OK));
365
366 let output = unsafe { core::slice::from_raw_parts(buf_ptr, 4096) };
368 let len = output.iter().position(|&b| b == b'\0').unwrap_or(output.len());
369 let output = &output[..len];
370
371 expect_true!(contains_command_help(
373 output,
374 b"mock_avail_always",
375 b"mock_avail_always help"
376 ));
377
378 expect_false!(contains_command_help(output, b"mock_success", b"mock_success help"));
380 }
381}
382
383#[unsafe(no_mangle)]
389pub unsafe extern "C" fn command_visibility_from_rust_test() -> bool {
390 unsafe {
391 let start = &__start_commands as *const console_rust::Cmd;
392 let stop = &__stop_commands as *const console_rust::Cmd;
393 let len = stop.offset_from(start) as usize;
394 let commands = core::slice::from_raw_parts(start, len);
395
396 for cmd in commands {
397 if core::ffi::CStr::from_ptr(cmd.cmd_str) == c"mock_success" {
398 if core::ffi::CStr::from_ptr(cmd.help_str) != c"mock_success help" {
399 return false;
400 }
401 if cmd.availability_mask != CMD_AVAIL_NORMAL {
402 return false;
403 }
404 return true;
405 }
406 }
407 }
408 false
409}