From a38d17997147e1a9abbaa4f3073aac785fa16a5b Mon Sep 17 00:00:00 2001
From: Ystinia Zalatova <94921687+ystinia@users.noreply.github.com>
Date: Wed, 24 Jul 2024 20:23:32 +0300
Subject: [PATCH] Add Miniature Shell for limited commands (#1499)

---
 general/package/Config.in           |   1 +
 general/package/lshell/Config.in    |   7 +
 general/package/lshell/lshell.mk    |  23 ++
 general/package/lshell/src/Makefile |   4 +
 general/package/lshell/src/lshell.c | 419 ++++++++++++++++++++++++++++
 5 files changed, 454 insertions(+)
 create mode 100644 general/package/lshell/Config.in
 create mode 100644 general/package/lshell/lshell.mk
 create mode 100644 general/package/lshell/src/Makefile
 create mode 100644 general/package/lshell/src/lshell.c

diff --git a/general/package/Config.in b/general/package/Config.in
index bfc15952..39096f2a 100644
--- a/general/package/Config.in
+++ b/general/package/Config.in
@@ -58,6 +58,7 @@ source "$BR2_EXTERNAL_GENERAL_PATH/package/libwebsockets-openipc/Config.in"
 source "$BR2_EXTERNAL_GENERAL_PATH/package/linux-firmware-openipc/Config.in"
 source "$BR2_EXTERNAL_GENERAL_PATH/package/linux-patcher/Config.in"
 source "$BR2_EXTERNAL_GENERAL_PATH/package/logcat/Config.in"
+source "$BR2_EXTERNAL_GENERAL_PATH/package/lshell/Config.in"
 source "$BR2_EXTERNAL_GENERAL_PATH/package/majestic-fonts/Config.in"
 source "$BR2_EXTERNAL_GENERAL_PATH/package/majestic-plugins/Config.in"
 source "$BR2_EXTERNAL_GENERAL_PATH/package/majestic-webui/Config.in"
diff --git a/general/package/lshell/Config.in b/general/package/lshell/Config.in
new file mode 100644
index 00000000..f785b530
--- /dev/null
+++ b/general/package/lshell/Config.in
@@ -0,0 +1,7 @@
+config BR2_PACKAGE_LSHELL
+	bool "lshell"
+	default n
+	help
+	  Miniature Shell for limited commands
+
+	  https://github.com/ystinia/sandbox/tree/main/lshell
diff --git a/general/package/lshell/lshell.mk b/general/package/lshell/lshell.mk
new file mode 100644
index 00000000..b11d567f
--- /dev/null
+++ b/general/package/lshell/lshell.mk
@@ -0,0 +1,23 @@
+################################################################################
+#
+# lshell | updated 2024.07.13
+#
+################################################################################
+
+LSHELL_LICENSE = GPL
+LSHELL_LICENSE_FILES = LICENSE
+
+define LSHELL_EXTRACT_CMDS
+	cp -a $(LSHELL_PKGDIR)/* $(@D)/
+endef
+
+define LSHELL_BUILD_CMDS
+	$(TARGET_CONFIGURE_OPTS) $(MAKE) -C $(@D)/src
+endef
+
+define LSHELL_INSTALL_TARGET_CMDS
+	$(INSTALL) -m 755 -d $(TARGET_DIR)/bin
+	$(INSTALL) -m 755 -D $(@D)/src/lshell $(TARGET_DIR)/bin/lshell
+endef
+
+$(eval $(generic-package))
diff --git a/general/package/lshell/src/Makefile b/general/package/lshell/src/Makefile
new file mode 100644
index 00000000..aec0350e
--- /dev/null
+++ b/general/package/lshell/src/Makefile
@@ -0,0 +1,4 @@
+
+lshell: lshell.c
+	$(CC) $(CFLAGS) -o lshell lshell.c -Wall -g
+	# $(CC) $(CFLAGS) -o lshell lshell.c -Wall -fsanitize=address -fsanitize=leak -g
diff --git a/general/package/lshell/src/lshell.c b/general/package/lshell/src/lshell.c
new file mode 100644
index 00000000..a6f07a1e
--- /dev/null
+++ b/general/package/lshell/src/lshell.c
@@ -0,0 +1,419 @@
+/**
+ * Based on a project from @conorbros
+ */
+
+#include <signal.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/wait.h>
+
+#define DEFAULT_PATH "/usr/share/openipc"
+
+char **paths;
+size_t paths_c = 0;
+
+/**
+ * @brief Adds a path to the list of paths to look for executables in
+ * 
+ * @param path 
+ */
+void add_path(char *path)
+{
+    paths[paths_c] = (char *)malloc(sizeof(char) * strlen(path) + 1);
+    strcpy(paths[paths_c++], path);
+    paths = realloc(paths, (paths_c + 1) * sizeof(char *));
+}
+
+/**
+ * @brief Initializes the list of paths and adds the DEFAULT_PATH
+ * 
+ */
+void init_paths()
+{
+    paths = (char **)malloc(sizeof(char *) * 2);
+    paths[paths_c] = (char *)malloc(sizeof(char) * strlen(DEFAULT_PATH) + 1);
+    strcpy(paths[paths_c++], DEFAULT_PATH);
+}
+
+/**
+ * @brief Empties the list of paths and frees memory
+ * 
+ */
+void clear_paths()
+{
+    paths = realloc(paths, sizeof(char *));
+    paths_c = 0;
+}
+
+void free_paths()
+{
+    for (int i = 0; i < paths_c; i++)
+    {
+        free(paths[i]);
+    }
+    free(paths);
+}
+
+/**
+ * @brief Frees all memory in a heap allocated char array
+ * 
+ * @param arr array to free
+ * @param c count of elements in array
+ */
+void free_char_arr(char **arr, size_t c)
+{
+    for (size_t i = 0; i < c; i++)
+    {
+        free(arr[i]);
+    }
+    free(arr);
+}
+
+/**
+ * @brief Prints an error msg to stderr
+ * 
+ */
+void print_err()
+{
+    char error_message[30] = "Sorry, unknown command\n";
+    write(STDERR_FILENO, error_message, strlen(error_message));
+}
+
+/**
+ * @brief Searches the shell's current paths for an executable matching the cmd
+ * 
+ * @param cmd 
+ * @return char* full path of the executable
+ */
+char *search_paths(char *cmd)
+{
+    char *res = NULL;
+    for (size_t i = 0; i < paths_c; i++)
+    {
+        size_t len = strlen(cmd) + strlen(paths[i]) + 3;
+        char *full_path = (char *)malloc(sizeof(char) * len);
+        sprintf(full_path, "%s/%s", paths[i], cmd);
+
+        if (access(full_path, X_OK) == 0)
+        {
+            res = (char *)malloc(sizeof(char) * (strlen(full_path) + 1));
+            strcpy(res, full_path);
+            free(full_path);
+            break;
+        }
+        free(full_path);
+    }
+    return res;
+}
+
+/**
+ * @brief Executes the command with arguments
+ * 
+ * @param cmd name of binary to execute
+ * @param args arguments to pass to executed program
+ * @param arg_c count of argument array
+ * @param redirect file to redirect stdout and stderr to (can be NULL)
+ * @return pid_t pid of process running the command
+ */
+pid_t exec_cmd(char *cmd, char **args, int arg_c, char *redirect)
+{
+    char *path = search_paths(cmd);
+    if (path == NULL)
+    {
+        print_err();
+        return 0;
+    }
+
+    pid_t pid = fork();
+    if (pid == -1)
+    {
+        perror("fork");
+        exit(1);
+    }
+
+    if (pid == 0)
+    {
+        if (redirect != NULL)
+        {
+            FILE *redirect_file;
+            redirect_file = fopen(redirect, "w");
+            if (redirect_file == NULL)
+            {
+                print_err();
+                exit(1);
+            }
+            dup2(fileno(redirect_file), STDOUT_FILENO);
+            dup2(fileno(redirect_file), STDERR_FILENO);
+        }
+        if (execv(path, args) == -1)
+        {
+            perror("execv");
+            exit(1);
+        }
+    }
+    free(path);
+    return pid;
+}
+/**
+ * @brief Gets the path to redirect output to. Will assign NULL to char array if no redirect specified. Function will return 0 for no errors, 1 if there is errors;
+ * 
+ * @param str 
+ * @param redirect
+ * @return int 0 for fine, 1 for errors
+ */
+int get_redirect_file(char *str, char **redirect)
+{
+    size_t o = 0;
+    int res = 0;
+    size_t len = strlen(str) + 1;
+    bool has_output = false;
+    int token_index = -1;
+
+    for (int i = 0; i < len; i++)
+    {
+
+        if (str[i] == '>' && i == 0)
+        {
+            return 1;
+        }
+        else if (str[i] == '>')
+        {
+            token_index = i;
+            has_output = true;
+            *redirect = (char *)malloc(sizeof(char) * (len));
+            if (str[i + 1] == ' ') // If user has put a space in between '>' and the filename, move to first char in filename
+            {
+                i++;
+            }
+            continue;
+        }
+
+        if (has_output && o > 1 && str[i] == ' ')
+        {
+            free(*redirect);
+            *redirect = NULL;
+
+            print_err();
+            exit(0);
+        }
+        else if (has_output)
+        {
+            (*redirect)[o] = str[i];
+            o++;
+        }
+    }
+    if (token_index != -1)
+    {
+        str[token_index] = 0;
+    }
+
+    return res;
+}
+
+/**
+ * @brief Parses and runs a string containing a cmd and optional parameters
+ * 
+ * @param cmd_str 
+ * @return pid_t pid of process running command
+ */
+pid_t handle_cmd(char *cmd_str)
+{
+    pid_t res = 0;
+    char *redirect = NULL;
+    if (get_redirect_file(cmd_str, &redirect) != 0)
+    {
+        print_err();
+        return 1;
+    }
+
+    char *cmd = NULL;
+    cmd = strtok(cmd_str, " ");
+    if (cmd == NULL)
+    {
+        return res;
+    }
+
+    char **args = (char **)malloc(sizeof(char *) * 2);
+    args[0] = strdup(cmd);
+    size_t args_n = 1;
+
+    char *token = NULL;
+    token = strtok(NULL, " ");
+    while (token != NULL)
+    {
+        args[args_n] = (char *)malloc(sizeof(char) * strlen(token) + 1);
+        strcpy(args[args_n++], token);
+
+        args = (char **)realloc(args, (args_n + 1) * sizeof(char *));
+        token = strtok(NULL, " ");
+    }
+
+    // null terminate the arg array
+    args = realloc(args, (args_n + 1) * sizeof(char *));
+    args[args_n++] = NULL;
+
+    if (strcmp(cmd, "cd") == 0) // Check if commands are built-in commands
+    {
+
+        if (args_n != 3) // If there is less 2 args (program name[0] and directory[1]) cannot run
+        {
+            print_err();
+            res = 0;
+        }
+        else
+        {
+            if (chdir(args[1]) == -1)
+            {
+                print_err();
+            }
+            res = 0;
+        }
+    }
+    else if (strcmp(cmd, "path") == 0)
+    {
+        clear_paths();
+
+        for (size_t i = 1; i < args_n - 1; i++) // -1 because arg array is null terminated
+        {
+            add_path(args[i]);
+        }
+        res = 0;
+    }
+    else // Else the command is a standard one
+    {
+        res = exec_cmd(cmd, args, args_n, redirect);
+    }
+
+    free_char_arr(args, args_n);
+    free(token);
+
+    return res;
+}
+
+int handle_line(char *line)
+{
+    if (strcmp(line, "exit") == 0)
+    {
+        return 1;
+    }
+
+    char *buffer = NULL;
+    char **cmds = (char **)malloc(sizeof(char *) * 2);
+
+    buffer = strtok(line, "&");
+    size_t cmds_c = 0;
+
+    while (buffer != NULL)
+    {
+
+        if (buffer[strlen(buffer) - 1] == ' ') // If last char is a space remove it
+        {
+            buffer[strlen(buffer) - 1] = 0;
+        }
+
+        if (buffer[0] == ' ') // If first char is a space remove it
+        {
+            buffer++;
+        }
+
+        // Put string containing cmd and parameters into array
+        cmds[cmds_c] = (char *)malloc(sizeof(char) * strlen(buffer) + 1);
+        strcpy(cmds[cmds_c++], buffer);
+        cmds = (char **)realloc(cmds, (cmds_c + 1) * sizeof(char *));
+        buffer = strtok(NULL, "&");
+    }
+
+    // Start all processes with `handle_cmd` function
+    for (size_t i = 0; i < cmds_c; i++)
+    {
+        handle_cmd(cmds[i]);
+    }
+
+    int status;
+    pid_t wpid;
+    while ((wpid = wait(&status)) > 0) // Wait for all child processes to finish before continuing
+    {
+    }
+
+    free_char_arr(cmds, cmds_c);
+    return 0;
+}
+
+void interactive_mode()
+{
+    char *buffer = NULL;
+    size_t len = 0;
+    while (1)
+    {
+        printf("keychain:~/jail> ");
+        fflush(stdout);
+
+        getline(&buffer, &len, stdin);
+        if (strcmp(buffer, "exit\n") == 0)
+        {
+            break;
+        }
+
+        if (buffer[strlen(buffer) - 1] == '\n') // Replace the newline with a null char
+        {
+            buffer[strlen(buffer) - 1] = 0;
+        }
+
+        if (handle_line(buffer) == 1)
+        {
+            break;
+        }
+    }
+    free(buffer);
+}
+
+void batch_mode(FILE *fp)
+{
+    char *buffer = NULL;
+    size_t len = 0;
+    while (getline(&buffer, &len, fp) > 0)
+    {
+        if (buffer[strlen(buffer) - 1] == '\n') // Replace the newline with a null char
+        {
+            buffer[strlen(buffer) - 1] = 0;
+        }
+
+        if (handle_line(buffer) == 1)
+        {
+            break;
+        }
+    }
+
+    free(buffer);
+}
+
+int main(int argc, char *argv[])
+{
+    FILE *input;
+
+    // Init the array of paths that
+    // the shell with look for executables in
+    init_paths();
+
+    if (argc > 1) // If arg is supplied use batch mode
+    {
+        input = fopen(argv[1], "r");
+        if (input == NULL || argc > 2) // If the input file doesn't exist or multiple input file arguments were given
+        {
+            print_err();
+            exit(1);
+        }
+
+        batch_mode(input);
+    }
+    else // else use interactive mode
+    {
+        interactive_mode();
+    }
+
+    free_paths();
+    exit(0);
+}