From a4826bd48c22ccff02a4d60d87cae13ed0f1238a Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Fri, 18 Apr 2025 12:34:45 +0000 Subject: [PATCH] Implement BSD's err.h --- src/header/err/cbindgen.toml | 5 + src/header/err/mod.rs | 205 +++++++++++++++++++++++++ src/header/mod.rs | 1 + tests/Makefile | 1 + tests/err.c | 51 ++++++ tests/expected/bins_dynamic/err.stderr | 6 + tests/expected/bins_dynamic/err.stdout | 2 + tests/expected/bins_static/err.stderr | 6 + tests/expected/bins_static/err.stdout | 2 + tests/run_tests.sh | 1 + 10 files changed, 280 insertions(+) create mode 100644 src/header/err/cbindgen.toml create mode 100644 src/header/err/mod.rs create mode 100644 tests/err.c create mode 100644 tests/expected/bins_dynamic/err.stderr create mode 100644 tests/expected/bins_dynamic/err.stdout create mode 100644 tests/expected/bins_static/err.stderr create mode 100644 tests/expected/bins_static/err.stdout diff --git a/src/header/err/cbindgen.toml b/src/header/err/cbindgen.toml new file mode 100644 index 0000000000..7319d182ab --- /dev/null +++ b/src/header/err/cbindgen.toml @@ -0,0 +1,5 @@ +sys_includes = ["stdarg.h", "stdio.h", "features.h"] +include_guard = "_RELIBC_ERR_H" +language = "C" +no_includes = true +cpp_compat = true diff --git a/src/header/err/mod.rs b/src/header/err/mod.rs new file mode 100644 index 0000000000..997266dd79 --- /dev/null +++ b/src/header/err/mod.rs @@ -0,0 +1,205 @@ +//! `err.h` implementation. +//! +//! See +//! +//! `err.h` is a BSD extension to the C library which provides functions for printing formatted +//! errors. Errors are printed to [`stdio::stderr`] by default or to a file set by +//! [`err_set_file`]. This family of functions is non-portable, but it is also supported by `glibc` +//! and `musl`. +//! +//! The functions come in sets of three. Each of them print the program binary name (the last path +//! segment of arg[0]) and an optional user message along with these differences: +//! * No suffix: Prints an error message for ERRNO based on [`strerror`] +//! * `c` suffix: Prints an error message for an arbitrary error code +//! * `x` suffix: Does not print an error code +//! +//! For example, `err` does not have a suffix so it would print the program name, the user message, +//! and an error string for ERRNO. `errc` would operate in the same way except the functions takes +//! an error code for which to print an error string. + +// Allow is intentional. Almost every line of the simple functions below are unsafe. +// unsafe_op_in_unsafe_fn only adds visual noise or a needless indentation here. +#![allow(unsafe_op_in_unsafe_fn)] + +use core::{ + ffi::{c_char, c_int, VaList as va_list}, + ptr, +}; + +use crate::{ + c_str::CStr, + header::{ + stdio::{self, fprintf, fputc, fputs, vfprintf, FILE}, + stdlib::exit, + string::strerror, + }, + platform::{self, ERRNO}, +}; + +// Optional callback from user invoked on exit. +type ExitCallback = Option; +static mut on_exit: ExitCallback = None; + +// Messages from this module are written to this sink. +static mut error_sink: *mut FILE = ptr::null_mut(); + +/// Set global [`FILE`] sink to write errors and warnings. +#[no_mangle] +pub unsafe extern "C" fn err_set_file(fp: *mut FILE) { + if fp.is_null() { + error_sink = stdio::stderr; + } else { + error_sink = fp; + } +} + +/// Set or remove a callback to invoke before exiting on error. +#[no_mangle] +pub unsafe extern "C" fn err_set_exit(ef: ExitCallback) { + on_exit = ef; +} + +/// Print a user message then an error message for [`ERRNO`] followed by exiting with `eval`. +/// +/// The message format is `progname: fmt: strerror(ERRNO)` +/// +/// # Return +/// Does not return. Exits with `eval` as an error code. +#[no_mangle] +pub unsafe extern "C" fn err(eval: c_int, fmt: *const c_char, mut va_list: ...) -> ! { + let code = Some(ERRNO.get()); + err_exit(eval, code, fmt, va_list.as_va_list()) +} + +/// Print a user message then an error message for `code` before exiting with `eval` as a return. +/// +/// The message format is `progname: fmt: strerror(code)` +/// +/// # Return +/// Exits with `eval` as an error code. +#[no_mangle] +pub unsafe extern "C" fn errc(eval: c_int, code: c_int, fmt: *const c_char, mut va_list: ...) -> ! { + err_exit(eval, Some(code), fmt, va_list.as_va_list()) +} + +/// Print a user message then exits with `eval` as a return. +/// +/// The message format is `progname: fmt` +/// +/// # Return +/// Exits with `eval` as an error code. +#[no_mangle] +pub unsafe extern "C" fn errx(eval: c_int, fmt: *const c_char, mut va_list: ...) -> ! { + err_exit(eval, None, fmt, va_list.as_va_list()) +} + +/// Print a user message and then an error message for [`ERRNO`]. +/// +/// The message format is `progname: fmt: strerror(ERRNO)` +#[no_mangle] +pub unsafe extern "C" fn warn(fmt: *const c_char, mut va_list: ...) { + let code = Some(ERRNO.get()); + display_message(code, fmt, va_list.as_va_list()); +} + +/// Print a user message then an error message for `code`. +/// +/// The message format is `progname: fmt: strerror(code)` +#[no_mangle] +pub unsafe extern "C" fn warnc(code: c_int, fmt: *const c_char, mut va_list: ...) { + display_message(Some(code), fmt, va_list.as_va_list()); +} + +/// Print a user message as a warning. +/// +/// The message format is `progname: fmt` +#[no_mangle] +pub unsafe extern "C" fn warnx(fmt: *const c_char, mut va_list: ...) { + display_message(None, fmt, va_list.as_va_list()); +} + +/// See [`err`]. +#[no_mangle] +pub unsafe extern "C" fn verr(eval: c_int, fmt: *const c_char, args: va_list) -> ! { + let code = Some(ERRNO.get()); + err_exit(eval, code, fmt, args); +} + +/// See [`errc`]. +#[no_mangle] +pub unsafe extern "C" fn verrc(eval: c_int, code: c_int, fmt: *const c_char, args: va_list) -> ! { + err_exit(eval, Some(code), fmt, args) +} + +/// See [`errx`]; +#[no_mangle] +pub unsafe extern "C" fn verrx(eval: c_int, fmt: *const c_char, args: va_list) -> ! { + err_exit(eval, None, fmt, args) +} + +/// See [`warn`]. +#[no_mangle] +pub unsafe extern "C" fn vwarn(fmt: *const c_char, args: va_list) { + let code = Some(ERRNO.get()); + display_message(code, fmt, args); +} + +/// See [`warnc`]. +#[no_mangle] +pub unsafe extern "C" fn vwarnc(code: c_int, fmt: *const c_char, args: va_list) { + display_message(Some(code), fmt, args); +} + +/// See [`warnx`]. +#[no_mangle] +pub unsafe extern "C" fn vwarnx(fmt: *const c_char, args: va_list) { + display_message(None, fmt, args); +} + +// Write error messages for err and warn to the currently set sink. +unsafe fn display_message(code: Option, fmt: *const c_char, args: va_list) { + /// SAFETY: + /// * error_sink is only null once on start but otherwise always stderr or a user set file + /// * User is trusted to pass in a valid file pointer if err_set_file is used + if error_sink.is_null() { + error_sink = stdio::stderr; + } + let sink = error_sink; + + // "progname:" is always printed + // SAFETY: + // * program_invocation_short_name is never null as it is set on start + // * program_invocation_short_name is not globally mutable so the user can't mangle it + fprintf( + sink, + c"%s".as_ptr(), + platform::program_invocation_short_name, + ); + + // Print user message if any + if !fmt.is_null() { + fputs(c": ".as_ptr(), sink); + vfprintf(sink, fmt, args); + } + + // Print error message for non-x functions + if let Some(code) = code { + let message = strerror(code); + fprintf(sink, c": %s".as_ptr(), message); + } + + // Always write new line + fputc(b'\n'.into(), sink); +} + +// Write an error message as per err and then exit. +unsafe fn err_exit(eval: c_int, code: Option, fmt: *const c_char, args: va_list) -> ! { + display_message(code, fmt, args); + + if let Some(callback) = on_exit { + // errx will hit the unwrap. + callback(code.unwrap_or_else(|| ERRNO.get())); + } + + exit(eval); +} diff --git a/src/header/mod.rs b/src/header/mod.rs index 79944ef285..acddf401de 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -18,6 +18,7 @@ pub mod dl_tls; pub mod dlfcn; pub mod elf; pub mod endian; +pub mod err; pub mod errno; pub mod fcntl; pub mod float; diff --git a/tests/Makefile b/tests/Makefile index a8317c5635..948215e198 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -15,6 +15,7 @@ EXPECT_NAMES=\ destructor \ dirent/scandir \ endian \ + err \ errno \ error \ fcntl/create \ diff --git a/tests/err.c b/tests/err.c new file mode 100644 index 0000000000..704edcc1b4 --- /dev/null +++ b/tests/err.c @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include + +__attribute__((nonnull(2))) +static void vwarn_test(int code, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + vwarnc(code, fmt, ap); + va_end(ap); +} + +static void log_to_stdout(void) { + err_set_file(stdout); + warnc(ENOENT, "Dang it, Bobby."); + err_set_file(NULL); +} + +void user_callback(int code) { + printf("Exiting due to error code: %d\n", code); +} + +int main(void) { + err_set_exit(user_callback); + + // Set errno to a known value for verifiable messages + // (Also, "Owner died" is just too funny not to use) + errno = EOWNERDEAD; + + warn("Ran out of coffee"); + warnx("%s pulled out your ethernet cable", "Cat"); + warnc(EACCES, "Eat %d cookies", 42); + + vwarn_test(EBADE, "Potato, %s", "krumpli"); + + // Set the sink to stdout then back to stderr + log_to_stdout(); + warnc(EPERM, + "I'm sorry, Dave. I'm afraid I can't do that." + ); + + // As long as one err function works they should all work since + // two functions handle everything internally. + errc(EXIT_SUCCESS, EUSERS, "Bye. It's crowded."); + + // Unreachable + puts("err did not exit"); + return EXIT_FAILURE; +} diff --git a/tests/expected/bins_dynamic/err.stderr b/tests/expected/bins_dynamic/err.stderr new file mode 100644 index 0000000000..b79d9c196f --- /dev/null +++ b/tests/expected/bins_dynamic/err.stderr @@ -0,0 +1,6 @@ +err: Ran out of coffee: Owner died +err: Cat pulled out your ethernet cable +err: Eat 42 cookies: Permission denied +err: Potato, krumpli: Invalid exchange +err: I'm sorry, Dave. I'm afraid I can't do that.: Operation not permitted +err: Bye. It's crowded.: Too many users diff --git a/tests/expected/bins_dynamic/err.stdout b/tests/expected/bins_dynamic/err.stdout new file mode 100644 index 0000000000..8ef72cb5bc --- /dev/null +++ b/tests/expected/bins_dynamic/err.stdout @@ -0,0 +1,2 @@ +err: Dang it, Bobby.: No such file or directory +Exiting due to error code: 87 diff --git a/tests/expected/bins_static/err.stderr b/tests/expected/bins_static/err.stderr new file mode 100644 index 0000000000..b79d9c196f --- /dev/null +++ b/tests/expected/bins_static/err.stderr @@ -0,0 +1,6 @@ +err: Ran out of coffee: Owner died +err: Cat pulled out your ethernet cable +err: Eat 42 cookies: Permission denied +err: Potato, krumpli: Invalid exchange +err: I'm sorry, Dave. I'm afraid I can't do that.: Operation not permitted +err: Bye. It's crowded.: Too many users diff --git a/tests/expected/bins_static/err.stdout b/tests/expected/bins_static/err.stdout new file mode 100644 index 0000000000..8ef72cb5bc --- /dev/null +++ b/tests/expected/bins_static/err.stdout @@ -0,0 +1,2 @@ +err: Dang it, Bobby.: No such file or directory +Exiting due to error code: 87 diff --git a/tests/run_tests.sh b/tests/run_tests.sh index f846948c87..af32abdc75 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -17,6 +17,7 @@ EXPECT_NAMES=(\ destructor \ dirent/scandir \ endian \ + err \ errno \ error \ fcntl/create \