diff --git a/Cargo.lock b/Cargo.lock index 2e6dc39..0620e83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,23 +2,31 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ns_cmd" +version = "0.2.6" +dependencies = [ + "ns_error", + "ns_string", +] + [[package]] name = "ns_data" -version = "0.2.1" +version = "0.2.6" dependencies = [ "ns_error", ] [[package]] name = "ns_error" -version = "0.2.1" +version = "0.2.6" dependencies = [ "thiserror", ] [[package]] name = "ns_io" -version = "0.2.1" +version = "0.2.6" dependencies = [ "ns_error", "ns_string", @@ -26,7 +34,7 @@ dependencies = [ [[package]] name = "ns_string" -version = "0.2.1" +version = "0.2.6" dependencies = [ "ns_error", ] diff --git a/Cargo.toml b/Cargo.toml index 5e05b5a..d15d809 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,12 @@ members = [ "crates/ns_io", "crates/ns_string", "crates/ns_error", - "crates/ns_data" + "crates/ns_data", + "crates/ns_cmd", ] # Shared metadata [workspace.package] -version = "0.2.1" +version = "0.2.6" edition = "2024" authors = ["Vaishnav Sabari Girish"] diff --git a/Makefile b/Makefile index 990fe5a..96d4d22 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ LIBS_TO_INSTALL = $(RUST_DIR)/libns_data.a \ # Added -Iinclude so C finds your new header folder locally INCLUDES = -I. -Iinclude # Added -lns_strings and -lns_data to link the Rust crates -LIBS = -L$(RUST_DIR) -lns_data -lns_io -lns_string -lns_error -lpthread -ldl -lm -Wl,-rpath=$(RUST_DIR) +LIBS = -L$(RUST_DIR) -lns_cmd -lns_data -lns_io -lns_string -lns_error -lpthread -ldl -lm -Wl,-rpath=$(RUST_DIR) EXAMPLES = $(patsubst $(EXAMPLE_DIR)/%.c,%,$(wildcard $(EXAMPLE_DIR)/*.c)) diff --git a/README.md b/README.md index fee5ec2..00c6805 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ allowing you to use it in any C project globally. See [BUILD.md](./BUILD.md) for prerequisite dependencies. ```bash -git clone [https://github.com/NextStd/NextStd.git](https://github.com/NextStd/NextStd.git) -cd NextStd +git clone https://github.com/NextStd/nextstd.git +cd nextstd sudo make install ``` diff --git a/ROADMAP.md b/ROADMAP.md index e7462b3..a540844 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -40,7 +40,7 @@ These features will finalize the core `ns_io` and `ns_string` modules. Replacing standard C's `fork()`, `exec()`, and the highly insecure `system()` calls with safe, memory-managed alternatives. -* [ ] **The Better `system()` (`ns_cmd`):** A high-level execution macro that +* [x] **The Better `system()` (`ns_cmd`):** A high-level execution macro that prevents shell injection and captures output safely without POSIX pipes. * *Architecture:* Introduces an `ns_cmd_output` struct containing separated `ns_string stdout` and `ns_string stderr` fields. This allows developers to diff --git a/crates/ns_cmd/Cargo.toml b/crates/ns_cmd/Cargo.toml new file mode 100644 index 0000000..e2e8fa3 --- /dev/null +++ b/crates/ns_cmd/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ns_cmd" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[lib] +crate-type = ["staticlib"] + +[dependencies] +ns_error = { path = "../ns_error" } +ns_string = { path = "../ns_string" } diff --git a/crates/ns_cmd/src/cmd.rs b/crates/ns_cmd/src/cmd.rs new file mode 100644 index 0000000..6e1fdb0 --- /dev/null +++ b/crates/ns_cmd/src/cmd.rs @@ -0,0 +1,124 @@ +use std::ffi::CStr; +use std::mem::ManuallyDrop; +use std::os::raw::{c_char, c_int}; +use std::process::Command; + +use ns_error::NsError; + +use ns_string::{NsString, NsStringData, NsStringHeap, ns_string_free}; + +#[repr(C)] +pub struct NsCmdOutput { + pub stdout_data: NsString, + pub stderr_data: NsString, + pub exit_code: c_int, +} + +fn rust_string_to_ns(s: String) -> NsString { + let bytes = s.into_bytes(); + let len = bytes.len(); + + if len < 24 { + let mut inline = [0; 24]; + inline[..len].copy_from_slice(&bytes); + NsString { + len, + is_heap: false, + data: NsStringData { + inline_data: inline, + }, + } + } else { + let mut vec = bytes; + vec.shrink_to_fit(); + let ptr = vec.as_mut_ptr(); + let capacity = vec.capacity(); + std::mem::forget(vec); + + NsString { + len, + is_heap: true, + data: NsStringData { + heap: ManuallyDrop::new(NsStringHeap { ptr, capacity }), + }, + } + } +} + +// Extract a Rust &str from NextStd String Union +fn ns_to_rust_str(ns: &NsString) -> &str { + let slice = unsafe { + if ns.is_heap { + std::slice::from_raw_parts(ns.data.heap.ptr, ns.len) + } else { + std::slice::from_raw_parts(ns.data.inline_data.as_ptr(), ns.len) + } + }; + + // Invalid command returns empty string + std::str::from_utf8(slice).unwrap_or("") +} + +// CORE EXECUTION +fn execute_shell_command(cmd_str: &str, output: *mut NsCmdOutput) -> NsError { + if output.is_null() { + return NsError::Any; + } + + // Spawn the process safely using the system shell + let process_result = Command::new("sh").arg("-c").arg(cmd_str).output(); + + match process_result { + Ok(proc_output) => { + let stdout_str = String::from_utf8_lossy(&proc_output.stdout).into_owned(); + let stderr_str = String::from_utf8_lossy(&proc_output.stderr).into_owned(); + + unsafe { + (*output).stdout_data = rust_string_to_ns(stdout_str); + (*output).stderr_data = rust_string_to_ns(stderr_str); + (*output).exit_code = proc_output.status.code().unwrap_or(-1); + } + NsError::Success + } + Err(_) => NsError::Any, + } +} + +// FFI exports +/// # Safety +/// +/// Runs a command if the input is a *const char in C +#[unsafe(no_mangle)] +pub unsafe extern "C" fn ns_cmd_run_cstr( + command: *const c_char, + output: *mut NsCmdOutput, +) -> NsError { + if command.is_null() { + return NsError::Any; + } + + let cmd_str = unsafe { CStr::from_ptr(command) }.to_string_lossy(); + + execute_shell_command(&cmd_str, output) +} + +#[unsafe(no_mangle)] +pub extern "C" fn ns_cmd_run_ns(command: NsString, output: *mut NsCmdOutput) -> NsError { + let cmd_str = ns_to_rust_str(&command); + execute_shell_command(cmd_str, output) +} + +/// # Safety +/// +/// This function frees the strings in the output struct +#[unsafe(no_mangle)] +pub unsafe extern "C" fn ns_cmd_output_free(output: *mut NsCmdOutput) { + if output.is_null() { + return; + } + + unsafe { + ns_string_free(&mut (*output).stdout_data); + ns_string_free(&mut (*output).stderr_data); + } +} diff --git a/crates/ns_cmd/src/lib.rs b/crates/ns_cmd/src/lib.rs new file mode 100644 index 0000000..c1d38fe --- /dev/null +++ b/crates/ns_cmd/src/lib.rs @@ -0,0 +1,3 @@ +pub mod cmd; + +pub use cmd::*; diff --git a/examples/15_cmd.c b/examples/15_cmd.c new file mode 100644 index 0000000..d111f5e --- /dev/null +++ b/examples/15_cmd.c @@ -0,0 +1,48 @@ +#include "../include/ns.h" +#include "../include/ns_cmd.h" + +int main(void) +{ + ns_println("NextStd Command Execution Demo"); + ns_println("Test 1: Standard C String Execution"); + { + // Zero initialize to ensure safe auto-cleanup + ns_autocmd ns_cmd_output out1 = {0}; + + ns_println("Executing uname -a"); + ns_cmd_run("uname -a", &out1); + + ns_println("Exit Code: {}", out1.exit_code); + ns_println("Stdout: {}", out1.stdout_data); + } + + ns_println("\nTest 2: Dynamic ns_string execution"); + { + ns_autocmd ns_cmd_output out2 = {0}; + + ns_string my_cmd; + ns_string_new(&my_cmd, "echo 'Command execution is now memory safe'"); + + ns_println("Executing echo 'Command execution is now memory safe'"); + ns_cmd_run(my_cmd, &out2); + + ns_println("Exit Code: {}", out2.exit_code); + ns_println("Stdout: {}", out2.stdout_data); + + // Free the input string + ns_string_free(&my_cmd); + } + + ns_println("\nTest 3: Stderr"); + { + ns_autocmd ns_cmd_output out_err = {0}; + + ns_println("Executing: cat non_existent_file.txt"); + ns_cmd_run("cat non_existent_file", &out_err); + + ns_println("Exit Code: {}", out_err.exit_code); + ns_println("Stderr: {}", out_err.stderr_data); + } + + return 0; +} diff --git a/include/ns_cmd.h b/include/ns_cmd.h new file mode 100644 index 0000000..cb7ac15 --- /dev/null +++ b/include/ns_cmd.h @@ -0,0 +1,45 @@ +#ifndef NS_CMD_H +#define NS_CMD_H + +#include "ns_string.h" +#include "ns_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + + typedef struct ns_cmd_output { + ns_string stdout_data; + ns_string stderr_data; + int exit_code; + } ns_cmd_output; + + // For both string types + ns_error_t ns_cmd_run_cstr(const char* command, ns_cmd_output* output); + ns_error_t ns_cmd_run_ns(ns_string command, ns_cmd_output* output); + + // Destructor function to auto free memory of the strings in the struct + void ns_cmd_output_free(ns_cmd_output* output); + + // Cleanup function + static inline void ns_cmd_cleanup_helper(ns_cmd_output* ptr) { + if (ptr) { + ns_cmd_output_free(ptr); + } + } + + // Routes the command based on string type +#define ns_cmd_run(cmd, out) _Generic((cmd), \ + char*: ns_cmd_run_cstr, \ + const char*: ns_cmd_run_cstr, \ + ns_string: ns_cmd_run_ns \ +)(cmd, out) + + // Auto free macro +#define ns_autocmd __attribute__((cleanup(ns_cmd_cleanup_helper))) + +#ifdef __cplusplus +} +#endif + +#endif // !NS_CMD_H