Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions crates/ns_cmd/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
124 changes: 124 additions & 0 deletions crates/ns_cmd/src/cmd.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions crates/ns_cmd/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod cmd;

pub use cmd::*;
48 changes: 48 additions & 0 deletions examples/15_cmd.c
Original file line number Diff line number Diff line change
@@ -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;
}
45 changes: 45 additions & 0 deletions include/ns_cmd.h
Original file line number Diff line number Diff line change
@@ -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