From b7a6b94f685b7c2b7dd3818a110b2c8150440d6b Mon Sep 17 00:00:00 2001 From: Siin0pe Date: Sun, 18 Oct 2026 13:55:54 +0200 Subject: [PATCH] Add ls program --- userspace/meson.build | 1 + userspace/programs/ls/main.c | 561 ++++++++++++++++++++++++++++++ userspace/programs/ls/meson.build | 13 + 3 files changed, 575 insertions(+) create mode 100644 userspace/programs/ls/main.c create mode 100644 userspace/programs/ls/meson.build diff --git a/userspace/meson.build b/userspace/meson.build index bfb02e2..97edc76 100644 --- a/userspace/meson.build +++ b/userspace/meson.build @@ -148,6 +148,7 @@ subdir('programs/text_input') subdir('programs/terminal') subdir('programs/tty_session') subdir('programs/echo') +subdir('programs/ls') subdir('programs/shell') subdir('programs/init') subdir('programs/bootfs') diff --git a/userspace/programs/ls/main.c b/userspace/programs/ls/main.c new file mode 100644 index 0000000..952d5ab --- /dev/null +++ b/userspace/programs/ls/main.c @@ -0,0 +1,561 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define VFS_PROTOCOL_VERSION_MAJOR 1u +#define VFS_PROTOCOL_VERSION_MINOR 0u + +enum { + LIST_PATH_CAPACITY = 4096u, +}; + +enum list_option { + LIST_OPTION_ALL = 1u << 0, + LIST_OPTION_ALMOST_ALL = 1u << 1, + LIST_OPTION_LONG = 1u << 2, + LIST_OPTION_ONE_PER_LINE = 1u << 3, +}; + +struct list_context { + cap_id_t vfs_cap; + cap_id_t working_directory_cap; + /* Zero when standard input is not a terminal: output is then one plain name per line. */ + size_t terminal_columns; + uint32_t options; +}; + +/* A lexically normalized path, relative to the working directory unless absolute. */ +struct list_path { + char bytes[LIST_PATH_CAPACITY]; + size_t size; + bool absolute; +}; + +struct list_item { + const uint8_t* name; + size_t name_size; + struct filesystem_node_info info; +}; + +struct list_directory { + const char* operand; + struct list_path path; + cap_id_t capability; + struct filesystem_node_info info; +}; + +/* A listing is built in memory and written at once, since every write to stdout is a terminal call. */ +struct list_output { + char* bytes; + size_t size; + size_t capacity; + bool failed; +}; + +static bool write_error(const char* operand, const char* message) { + if (operand == NULL) return fprintf(stderr, "ls: %s\n", message) >= 0; + return fprintf(stderr, "ls: %s: %s\n", operand, message) >= 0; +} + +static bool call_succeeded(struct filesystem_call_result result) { + return result.transport_status == SYSCALL_STATUS_OK && result.status == FILESYSTEM_STATUS_OK; +} + +static const char* filesystem_error_message(struct filesystem_call_result result) { + if (result.transport_status != SYSCALL_STATUS_OK) return "filesystem operation failed"; + switch (result.status) { + case FILESYSTEM_STATUS_NOT_FOUND: + return "no such file or directory"; + case FILESYSTEM_STATUS_NOT_DIRECTORY: + return "not a directory"; + case FILESYSTEM_STATUS_ACCESS_DENIED: + return "permission denied"; + case FILESYSTEM_STATUS_INVALID_ARGUMENT: + return "invalid path"; + default: + return "filesystem operation failed"; + } +} + +static bool remove_component(struct list_path* path) { + size_t root = path->absolute ? 1u : 0u; + + if (path->size == root) return path->absolute; + while (path->size > root && path->bytes[path->size - 1u] != '/') path->size--; + if (path->size > root) path->size--; + return true; +} + +/* Returns NULL on success, or the message describing why the path cannot be used. */ +static const char* normalize_path(const char* operand, struct list_path* out) { + size_t length = strlen(operand); + size_t offset = 0u; + + out->absolute = length != 0u && operand[0] == '/'; + out->size = 0u; + if (out->absolute) out->bytes[out->size++] = '/'; + while (offset < length) { + size_t start; + size_t size; + size_t separator_size; + + while (offset < length && operand[offset] == '/') offset++; + start = offset; + while (offset < length && operand[offset] != '/') offset++; + size = offset - start; + if (size == 0u || (size == 1u && operand[start] == '.')) continue; + if (size == 2u && operand[start] == '.' && operand[start + 1u] == '.') { + /* Programs only hold the working directory, not a path to its parent. */ + if (!remove_component(out)) return "parent of the working directory is not reachable"; + continue; + } + separator_size = out->size > (out->absolute ? 1u : 0u) ? 1u : 0u; + if (size > FILESYSTEM_NAME_MAX || separator_size + size > sizeof(out->bytes) - out->size) return "invalid path"; + if (separator_size != 0u) out->bytes[out->size++] = '/'; + memcpy(out->bytes + out->size, operand + start, size); + out->size += size; + } + return NULL; +} + +static bool acquire_vfs(struct list_context* context) { + static const struct init_protocol_query query = { + .namespace_path = VFS_NAMESPACE, + .protocol = VFS_PROTOCOL_NAME, + .major = VFS_PROTOCOL_VERSION_MAJOR, + .minor = VFS_PROTOCOL_VERSION_MINOR, + }; + struct init_service_handle service = {.capability = CAP_ID_INVALID}; + struct init_call_result acquired; + + if (context->vfs_cap != CAP_ID_INVALID) return true; + acquired = init_acquire(&query, VFS_SERVICE_NAME, &service); + if (acquired.transport_status != SYSCALL_STATUS_OK || acquired.status != INIT_REGISTRY_OK) { + if (service.capability != CAP_ID_INVALID) (void)cap_drop(service.capability); + return false; + } + context->vfs_cap = service.capability; + return context->vfs_cap != CAP_ID_INVALID; +} + +/* The working directory itself is borrowed rather than opened, so release it with release_node. */ +static struct filesystem_call_result open_path(struct list_context* context, const struct list_path* path, + struct filesystem_node_handle* out_node) { + const struct filesystem_call_result failed = {.status = FILESYSTEM_STATUS_NONE, + .transport_status = SYSCALL_STATUS_FAILED}; + + *out_node = (struct filesystem_node_handle){.capability = CAP_ID_INVALID}; + if (path->absolute) { + if (!acquire_vfs(context)) return failed; + return vfs_open(context->vfs_cap, path->bytes, path->size, CAP_READ, out_node); + } + if (context->working_directory_cap == CAP_ID_INVALID) return failed; + if (path->size != 0u) + return vfs_directory_open(context->working_directory_cap, path->bytes, path->size, CAP_READ, out_node); + out_node->capability = context->working_directory_cap; + return vfs_directory_info(context->working_directory_cap, &out_node->info); +} + +static void release_node(const struct list_context* context, cap_id_t capability) { + if (capability != CAP_ID_INVALID && capability != context->working_directory_cap) (void)cap_drop(capability); +} + +static void output_append(struct list_output* output, const void* bytes, size_t size) { + if (output->failed || size == 0u) return; + if (size > output->capacity - output->size) { + size_t capacity = output->capacity == 0u ? 256u : output->capacity; + char* grown; + + while (size > capacity - output->size) { + if (capacity > SIZE_MAX / 2u) { + output->failed = true; + return; + } + capacity *= 2u; + } + grown = realloc(output->bytes, capacity); + if (grown == NULL) { + output->failed = true; + return; + } + output->bytes = grown; + output->capacity = capacity; + } + memcpy(output->bytes + output->size, bytes, size); + output->size += size; +} + +static void output_append_padding(struct list_output* output, size_t count) { + static const char spaces[] = " "; + + while (count != 0u) { + size_t chunk = count < sizeof(spaces) - 1u ? count : sizeof(spaces) - 1u; + + output_append(output, spaces, chunk); + count -= chunk; + } +} + +static bool flush_output(struct list_output* output) { + bool success; + + if (output->failed) success = write_error(NULL, "out of memory"); + else success = output->size == 0u || fwrite(output->bytes, 1u, output->size, stdout) == output->size; + free(output->bytes); + *output = (struct list_output){0}; + return success; +} + +static size_t item_width(const struct list_item* item) { + size_t width = 0u; + + for (size_t i = 0u; i < item->name_size; i++) { + if ((item->name[i] & 0xc0u) != 0x80u) width++; + } + return width; +} + +/* Size of the C0, DEL or UTF-8 encoded C1 control character at index, or zero for any other byte. */ +static size_t control_size(const uint8_t* name, size_t size, size_t index) { + if (name[index] < 0x20u || name[index] == 0x7fu) return 1u; + if (name[index] == 0xc2u && index + 1u < size && name[index + 1u] >= 0x80u && name[index + 1u] <= 0x9fu) return 2u; + return 0u; +} + +static void output_append_name(struct list_output* output, const struct list_context* context, + const struct list_item* item) { + bool colored = context->terminal_columns != 0u && item->info.type == FILESYSTEM_NODE_DIRECTORY; + size_t start = 0u; + + if (colored) output_append(output, "\x1b[1;34m", 7u); + /* Control characters in a name would otherwise be interpreted by the terminal. */ + for (size_t i = 0u; i < item->name_size;) { + size_t control = control_size(item->name, item->name_size, i); + + if (control == 0u) { + i++; + continue; + } + output_append(output, item->name + start, i - start); + output_append(output, "?", 1u); + i += control; + start = i; + } + output_append(output, item->name + start, item->name_size - start); + if (colored) output_append(output, "\x1b[0m", 4u); +} + +static void output_append_long(struct list_output* output, const struct list_context* context, + const struct list_item* item, size_t size_width) { + char size[24]; + int size_length = snprintf(size, sizeof(size), "%llu", (unsigned long long)item->info.size); + char mode[] = {item->info.type == FILESYSTEM_NODE_DIRECTORY ? 'd' : '-', + 'r', + (item->info.flags & FILESYSTEM_NODE_READ_ONLY) != 0u ? '-' : 'w', + ' '}; + + output_append(output, mode, sizeof(mode)); + output_append_padding(output, size_width - (size_t)size_length); + output_append(output, size, (size_t)size_length); + output_append(output, " ", 1u); + output_append_name(output, context, item); + output_append(output, "\n", 1u); +} + +static void output_append_items(struct list_output* output, const struct list_context* context, + const struct list_item* items, size_t count) { + size_t width = 0u; + size_t columns; + size_t rows; + + if (count == 0u) return; + if ((context->options & LIST_OPTION_LONG) != 0u) { + size_t size_width = 1u; + + for (size_t i = 0u; i < count; i++) { + size_t digits = 1u; + for (uint64_t size = items[i].info.size; size >= 10u; size /= 10u) digits++; + if (digits > size_width) size_width = digits; + } + for (size_t i = 0u; i < count; i++) output_append_long(output, context, &items[i], size_width); + return; + } + for (size_t i = 0u; i < count; i++) { + size_t current = item_width(&items[i]); + if (current > width) width = current; + } + width += 2u; + columns = (context->options & LIST_OPTION_ONE_PER_LINE) != 0u || width > context->terminal_columns + ? 1u + : context->terminal_columns / width; + rows = (count + columns - 1u) / columns; + /* Entries fill each column top to bottom before moving to the next one. */ + for (size_t row = 0u; row < rows; row++) { + for (size_t index = row; index < count; index += rows) { + output_append_name(output, context, &items[index]); + if (index + rows < count) output_append_padding(output, width - item_width(&items[index])); + } + output_append(output, "\n", 1u); + } +} + +static int compare_items(const void* left, const void* right) { + const struct list_item* left_item = left; + const struct list_item* right_item = right; + size_t size = left_item->name_size < right_item->name_size ? left_item->name_size : right_item->name_size; + int order = memcmp(left_item->name, right_item->name, size); + + if (order != 0) return order; + return (left_item->name_size > right_item->name_size) - (left_item->name_size < right_item->name_size); +} + +static int compare_directories(const void* left, const void* right) { + return strcmp(((const struct list_directory*)left)->operand, ((const struct list_directory*)right)->operand); +} + +/* Returns NULL on success, or the message describing the failure. */ +static const char* read_directory(cap_id_t directory_cap, struct filesystem_directory_entry** out_entries, + size_t* out_count) { + struct filesystem_directory_entry* entries = NULL; + size_t count = 0u; + size_t capacity = 0u; + uint64_t total = 0u; + + for (;;) { + struct filesystem_call_result result; + size_t returned; + + if (count == capacity) { + struct filesystem_directory_entry* grown; + size_t next = capacity == 0u ? 16u : capacity * 2u; + + if (total > next && total <= SIZE_MAX / sizeof(*entries)) next = (size_t)total; + grown = next > SIZE_MAX / sizeof(*entries) ? NULL : realloc(entries, next * sizeof(*entries)); + if (grown == NULL) { + free(entries); + return "out of memory"; + } + entries = grown; + capacity = next; + } + result = vfs_directory_enumerate(directory_cap, count, entries + count, capacity - count, &returned, &total); + if (!call_succeeded(result)) { + free(entries); + return filesystem_error_message(result); + } + count += returned; + if (returned == 0u || count >= total) break; + } + *out_entries = entries; + *out_count = count; + return NULL; +} + +static struct filesystem_node_info read_parent_info(struct list_context* context, + const struct list_directory* directory) { + struct filesystem_node_handle parent = {.capability = CAP_ID_INVALID}; + struct list_path* path = malloc(sizeof(*path)); + struct filesystem_node_info info = directory->info; + + if (path == NULL) return info; + *path = directory->path; + if (remove_component(path) && call_succeeded(open_path(context, path, &parent))) info = parent.info; + release_node(context, parent.capability); + free(path); + return info; +} + +/* Fill items with the entries ls shows for an opened directory. */ +static size_t collect_items(struct list_context* context, const struct list_directory* directory, + const struct filesystem_directory_entry* entries, size_t count, struct list_item* items) { + size_t item_count = 0u; + + if ((context->options & LIST_OPTION_ALL) != 0u) { + struct filesystem_node_info parent_info = {.type = FILESYSTEM_NODE_DIRECTORY}; + + /* Only the long format shows more of ".." than its type. */ + if ((context->options & LIST_OPTION_LONG) != 0u) parent_info = read_parent_info(context, directory); + items[item_count++] = (struct list_item){.name = (const uint8_t*)".", .name_size = 1u, .info = directory->info}; + items[item_count++] = (struct list_item){.name = (const uint8_t*)"..", .name_size = 2u, .info = parent_info}; + } + for (size_t i = 0u; i < count; i++) { + if (entries[i].name_size == 0u || + (entries[i].name[0] == '.' && (context->options & (LIST_OPTION_ALL | LIST_OPTION_ALMOST_ALL)) == 0u)) + continue; + items[item_count++] = + (struct list_item){.name = entries[i].name, .name_size = entries[i].name_size, .info = entries[i].info}; + } + qsort(items, item_count, sizeof(*items), compare_items); + return item_count; +} + +static bool list_directory(struct list_context* context, const struct list_directory* directory, bool header, + bool* printed) { + struct filesystem_directory_entry* entries; + struct list_item* items; + struct list_output output = {0}; + size_t count; + const char* error; + + error = read_directory(directory->capability, &entries, &count); + if (error != NULL) { + (void)write_error(directory->operand, error); + return false; + } + items = malloc((count + 2u) * sizeof(*items)); + if (items == NULL) { + free(entries); + (void)write_error(directory->operand, "out of memory"); + return false; + } + count = collect_items(context, directory, entries, count, items); + if (header) { + if (*printed) output_append(&output, "\n", 1u); + output_append(&output, directory->operand, strlen(directory->operand)); + output_append(&output, ":\n", 2u); + } + output_append_items(&output, context, items, count); + *printed = true; + free(items); + free(entries); + return flush_output(&output); +} + +/* Like GNU ls, options may follow operands; every argument after "--" is an operand. */ +static bool is_operand(const char* argument, bool after_separator) { + return after_separator || argument[0] != '-' || argument[1] == '\0'; +} + +static size_t terminal_columns(void) { + struct tty_session_get_info_response info; + cap_id_t stdin_cap; + + /* Standard output is write-only, so the terminal size is read through standard input. */ + display_get_standard_streams(&stdin_cap, NULL, NULL); + if (stdin_cap == CAP_ID_INVALID || tty_session_get_info(stdin_cap, &info) != SYSCALL_STATUS_OK || + info.columns > SIZE_MAX) + return 0u; + return (size_t)info.columns; +} + +/* + * Like GNU ls, unusable operands are reported first, then file operands are + * listed together, then each directory, all in name order. + */ +int main(int argc, char** argv, size_t capc, const cap_id_t* capv) { + static char current_name[] = "."; + static char* current_argv[] = {current_name}; + struct list_context context = {.vfs_cap = CAP_ID_INVALID, + .working_directory_cap = filesystem_get_working_directory()}; + struct list_item* files; + struct list_directory* directories; + struct list_output output = {0}; + size_t operand_count = 0u; + size_t file_count = 0u; + size_t directory_count = 0u; + bool after_separator = false; + bool printed = false; + bool success = true; + + if (argc < 0 || (argc != 0 && argv == NULL) || capc != 0u || capv != NULL) return 1; + for (int i = 0; i < argc; i++) { + if (is_operand(argv[i], after_separator)) { + operand_count++; + continue; + } + if (strcmp(argv[i], "--") == 0) { + after_separator = true; + continue; + } + if (argv[i][1] == '-') { + (void)fprintf(stderr, "ls: unknown option %s\n", argv[i]); + return 1; + } + for (const char* letter = argv[i] + 1; *letter != '\0'; letter++) { + if (*letter == 'a') + context.options = (context.options & ~(uint32_t)LIST_OPTION_ALMOST_ALL) | LIST_OPTION_ALL; + else if (*letter == 'A') + context.options = (context.options & ~(uint32_t)LIST_OPTION_ALL) | LIST_OPTION_ALMOST_ALL; + else if (*letter == 'l') context.options |= LIST_OPTION_LONG; + else if (*letter == '1') context.options |= LIST_OPTION_ONE_PER_LINE; + else { + (void)fprintf(stderr, "ls: unknown option -%c\n", *letter); + return 1; + } + } + } + if (operand_count == 0u) { + argc = 1; + argv = current_argv; + operand_count = 1u; + } + context.terminal_columns = terminal_columns(); + files = malloc(operand_count * sizeof(*files)); + directories = malloc(operand_count * sizeof(*directories)); + if (files == NULL || directories == NULL) { + free(directories); + free(files); + (void)write_error(NULL, "out of memory"); + return 1; + } + after_separator = false; + for (int i = 0; i < argc; i++) { + struct list_directory* directory = &directories[directory_count]; + struct filesystem_node_handle node; + struct filesystem_call_result result; + const char* error; + + if (!is_operand(argv[i], after_separator)) { + after_separator = after_separator || strcmp(argv[i], "--") == 0; + continue; + } + error = normalize_path(argv[i], &directory->path); + if (error != NULL) { + (void)write_error(argv[i], error); + success = false; + continue; + } + result = open_path(&context, &directory->path, &node); + if (!call_succeeded(result)) { + release_node(&context, node.capability); + (void)write_error(argv[i], filesystem_error_message(result)); + success = false; + continue; + } + if (node.info.type == FILESYSTEM_NODE_DIRECTORY) { + directory->operand = argv[i]; + directory->capability = node.capability; + directory->info = node.info; + directory_count++; + continue; + } + release_node(&context, node.capability); + files[file_count++] = + (struct list_item){.name = (const uint8_t*)argv[i], .name_size = strlen(argv[i]), .info = node.info}; + } + qsort(files, file_count, sizeof(*files), compare_items); + qsort(directories, directory_count, sizeof(*directories), compare_directories); + if (file_count != 0u) { + output_append_items(&output, &context, files, file_count); + success = flush_output(&output) && success; + printed = true; + } + for (size_t i = 0u; i < directory_count; i++) { + success = list_directory(&context, &directories[i], operand_count > 1u, &printed) && success; + release_node(&context, directories[i].capability); + } + free(directories); + free(files); + if (context.vfs_cap != CAP_ID_INVALID) (void)cap_drop(context.vfs_cap); + return success ? 0 : 1; +} diff --git a/userspace/programs/ls/meson.build b/userspace/programs/ls/meson.build new file mode 100644 index 0000000..02e3da5 --- /dev/null +++ b/userspace/programs/ls/meson.build @@ -0,0 +1,13 @@ +ls_elf = executable('ls.elf', + sources : files('main.c'), + c_args : userspace_c_args, + include_directories : userspace_inc, + dependencies : [tty_session_dep, vfs_dep], + link_whole : userspace_crt, + link_with : userspace_link_libs, + link_args : userspace_link_args, + install : false, + link_depends : files(userspace_linker_file), +) + +userspace_programs += ls_elf