Files
SUB-OS/userland/lazybox/coreutils.c
T
subhobhai943andClaude Opus 5 f9400f6ac4 feat(kernel): Add RB-tree/kfifo/hashtable, wait queues, futex, tiny RCU, page cache and KTest harness
Expands the kernel-and-TTY core with the primitives the higher layers have
been missing, plus an in-kernel harness that proves they work.

Data structures (lib/):
  - rbtree.c: Linux-compatible red-black tree with parent pointers, full
    insert/erase rebalancing, in-order iteration and an invariant validator.
  - kfifo.c: power-of-two byte ring with free-running indices, so the wrap
    path costs one mask instead of a branch per byte.
  - hashtable.c: separately-chained table with FNV-1a hashing, string or
    u64 keys, and rehash-on-growth at a 3/4 load factor.

Kernel subsystems:
  - kernel/wait.c: wait queues and one-shot completion barriers, backed by a
    static sleeper pool so a blocking path never re-enters the allocator.
  - kernel/futex.c: 32-bucket futex hash with value re-check, requeue, and an
    fmutex_t sleeping mutex built on the uncontended-fastpath protocol.
  - kernel/rcu.c: tiny RCU with reader nesting, deferred callbacks, kfree_rcu
    and rcu_assign_pointer/rcu_dereference publish barriers.
  - mm/page_cache.c: write-back block page cache, 4 KB pages indexed by
    (device, page), CLOCK second-chance eviction and dirty write-back.
  - kernel/ktest.c: KUnit-style harness with 6 built-in suites, 21 cases and
    735 assertions covering every subsystem above (all passing under QEMU).

LazyBox userland:
  - coreutils.c: sort, uniq, cut, tr, rev, tac, nl, seq, diff, xxd, du,
    factor, sum and truncate, reading through the VFS and handling the
    zero-length synthetic nodes that procfs and sysfs expose.
  - ktest, rcuinfo, futexinfo, waitinfo and pagecache diagnostics applets.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L5yKJT4Wo2cV3rfPrwY5MP
2026-08-24 23:38:59 +05:30

745 lines
22 KiB
C

// SUB-OS LazyBox Text Processing Suite
//
// GNU coreutils-compatible line and byte oriented tools operating over the
// VFS: sort, uniq, cut, tr, rev, tac, nl, seq, diff, xxd, du, factor, sum
// and truncate. Every tool that reads a file streams it into a bounded line
// table so a pathological input cannot exhaust the kernel heap.
#include <userland/coreutils.h>
#include <fs/vfs.h>
#include <kernel/printk.h>
#include <mm/kmalloc.h>
#include <lib/string.h>
#include <lib/printf.h>
#include <stdint.h>
#include <stdbool.h>
#define CU_MAX_LINES 512
#define CU_MAX_LINE_LEN 256
#define CU_MAX_FILE_SIZE (128 * 1024)
typedef struct {
char** lines;
int count;
char* raw;
} cu_text_t;
static void cu_free_text(cu_text_t* t) {
if (!t) return;
if (t->lines) kfree(t->lines);
if (t->raw) kfree(t->raw);
t->lines = NULL;
t->raw = NULL;
t->count = 0;
}
// Slurp a file and split it in place on newlines. Returns false (and prints a
// diagnostic) when the path cannot be read.
static bool cu_read_text(const char* path, cu_text_t* out) {
out->lines = NULL;
out->raw = NULL;
out->count = 0;
vfs_node_t* node = vfs_namei(path);
if (!node) {
printk(ANSI_RED "cannot open '%s': No such file or directory\n" ANSI_RESET, path);
return false;
}
if (node->flags & FS_DIRECTORY) {
printk(ANSI_RED "'%s' is a directory\n" ANSI_RESET, path);
return false;
}
// Synthetic procfs/sysfs nodes report length 0 and materialise their
// contents only on read, so fall back to a fixed window for those.
size_t len = node->length ? node->length : 8192;
if (len > CU_MAX_FILE_SIZE) len = CU_MAX_FILE_SIZE;
char* buf = (char*)kmalloc(len + 2);
if (!buf) {
printk(ANSI_RED "out of memory reading '%s'\n" ANSI_RESET, path);
return false;
}
int fd = vfs_open(path, O_RDONLY);
if (fd < 0) {
kfree(buf);
printk(ANSI_RED "cannot open '%s'\n" ANSI_RESET, path);
return false;
}
ssize_t got = vfs_read(fd, buf, len);
vfs_close(fd);
if (got < 0) got = 0;
buf[got] = '\0';
char** lines = (char**)kzalloc(CU_MAX_LINES * sizeof(char*));
if (!lines) {
kfree(buf);
return false;
}
int count = 0;
char* cursor = buf;
lines[count++] = cursor;
for (ssize_t i = 0; i < got && count < CU_MAX_LINES; i++) {
if (buf[i] == '\n') {
buf[i] = '\0';
if (i + 1 < got) lines[count++] = &buf[i + 1];
}
}
// A trailing newline produces one empty final line; drop it.
if (count > 0 && lines[count - 1][0] == '\0' && got > 0 && buf[got - 1] == '\0') {
count--;
}
out->raw = buf;
out->lines = lines;
out->count = count;
return true;
}
static int cu_atoi(const char* s, int fallback) {
if (!s || !*s) return fallback;
int sign = 1;
if (*s == '-') { sign = -1; s++; }
if (*s < '0' || *s > '9') return fallback;
int v = 0;
while (*s >= '0' && *s <= '9') {
v = v * 10 + (*s - '0');
s++;
}
return v * sign;
}
static bool cu_has_flag(int argc, char** argv, const char* flag) {
for (int i = 1; i < argc; i++) {
if (argv[i] && strcmp(argv[i], flag) == 0) return true;
}
return false;
}
// First argument that is not a flag and not the value of a flag.
static const char* cu_first_operand(int argc, char** argv) {
for (int i = 1; i < argc; i++) {
if (argv[i] && argv[i][0] != '-') return argv[i];
}
return NULL;
}
// ===========================================================================
// sort
// ===========================================================================
static int cu_strcmp_ci(const char* a, const char* b) {
while (*a && *b) {
char ca = (*a >= 'A' && *a <= 'Z') ? (char)(*a + 32) : *a;
char cb = (*b >= 'A' && *b <= 'Z') ? (char)(*b + 32) : *b;
if (ca != cb) return (int)(unsigned char)ca - (int)(unsigned char)cb;
a++; b++;
}
return (int)(unsigned char)*a - (int)(unsigned char)*b;
}
int coreutils_sort(int argc, char** argv) {
const char* path = cu_first_operand(argc, argv);
if (!path) {
printk("Usage: sort [-r] [-n] [-f] [-u] <file>\n");
printk(" -r reverse order -n numeric sort\n");
printk(" -f ignore case -u unique lines only\n");
return 1;
}
bool reverse = cu_has_flag(argc, argv, "-r");
bool numeric = cu_has_flag(argc, argv, "-n");
bool fold = cu_has_flag(argc, argv, "-f");
bool unique = cu_has_flag(argc, argv, "-u");
cu_text_t t;
if (!cu_read_text(path, &t)) return 1;
// Insertion sort: input is capped at CU_MAX_LINES, and it is stable,
// which `sort -u` relies on to keep the first of each duplicate run.
for (int i = 1; i < t.count; i++) {
char* key = t.lines[i];
int j = i - 1;
while (j >= 0) {
int cmp;
if (numeric) {
int a = cu_atoi(t.lines[j], 0);
int b = cu_atoi(key, 0);
cmp = (a > b) - (a < b);
} else if (fold) {
cmp = cu_strcmp_ci(t.lines[j], key);
} else {
cmp = strcmp(t.lines[j], key);
}
if (reverse) cmp = -cmp;
if (cmp <= 0) break;
t.lines[j + 1] = t.lines[j];
j--;
}
t.lines[j + 1] = key;
}
const char* prev = NULL;
for (int i = 0; i < t.count; i++) {
if (unique && prev && strcmp(prev, t.lines[i]) == 0) continue;
printk("%s\n", t.lines[i]);
prev = t.lines[i];
}
cu_free_text(&t);
return 0;
}
// ===========================================================================
// uniq
// ===========================================================================
int coreutils_uniq(int argc, char** argv) {
const char* path = cu_first_operand(argc, argv);
if (!path) {
printk("Usage: uniq [-c] [-d] [-u] <file>\n");
printk(" -c prefix each line with its repeat count\n");
printk(" -d print only duplicated lines\n");
printk(" -u print only unique lines\n");
return 1;
}
bool show_count = cu_has_flag(argc, argv, "-c");
bool dups_only = cu_has_flag(argc, argv, "-d");
bool uniq_only = cu_has_flag(argc, argv, "-u");
cu_text_t t;
if (!cu_read_text(path, &t)) return 1;
int i = 0;
while (i < t.count) {
int run = 1;
while (i + run < t.count && strcmp(t.lines[i], t.lines[i + run]) == 0) run++;
bool emit = true;
if (dups_only && run < 2) emit = false;
if (uniq_only && run > 1) emit = false;
if (emit) {
if (show_count) printk("%7d %s\n", run, t.lines[i]);
else printk("%s\n", t.lines[i]);
}
i += run;
}
cu_free_text(&t);
return 0;
}
// ===========================================================================
// cut
// ===========================================================================
int coreutils_cut(int argc, char** argv) {
const char* path = NULL;
char delim = '\t';
int field = 0;
int byte_start = 0, byte_end = 0;
for (int i = 1; i < argc; i++) {
if (!argv[i]) continue;
if (strcmp(argv[i], "-d") == 0 && i + 1 < argc) {
delim = argv[++i][0];
} else if (strcmp(argv[i], "-f") == 0 && i + 1 < argc) {
field = cu_atoi(argv[++i], 1);
} else if (strcmp(argv[i], "-b") == 0 && i + 1 < argc) {
const char* spec = argv[++i];
byte_start = cu_atoi(spec, 1);
const char* dash = strchr(spec, '-');
byte_end = dash ? cu_atoi(dash + 1, 0) : byte_start;
} else if (argv[i][0] != '-') {
path = argv[i];
}
}
if (!path || (field == 0 && byte_start == 0)) {
printk("Usage: cut -f <n> [-d <char>] <file>\n");
printk(" cut -b <start[-end]> <file>\n");
return 1;
}
cu_text_t t;
if (!cu_read_text(path, &t)) return 1;
for (int i = 0; i < t.count; i++) {
const char* line = t.lines[i];
if (byte_start > 0) {
int len = (int)strlen(line);
int from = byte_start - 1;
int to = (byte_end > 0) ? byte_end : len;
if (to > len) to = len;
for (int b = from; b < to; b++) printk("%c", line[b]);
printk("\n");
continue;
}
// Field mode: walk the delimiter runs and print the requested column.
int current = 1;
const char* start = line;
const char* p = line;
bool printed = false;
while (1) {
if (*p == delim || *p == '\0') {
if (current == field) {
for (const char* q = start; q < p; q++) printk("%c", *q);
printed = true;
break;
}
if (*p == '\0') break;
current++;
start = p + 1;
}
if (*p == '\0') break;
p++;
}
if (!printed && current < field) {
// Missing field: GNU cut emits an empty line.
}
printk("\n");
}
cu_free_text(&t);
return 0;
}
// ===========================================================================
// tr
// ===========================================================================
int coreutils_tr(int argc, char** argv) {
if (argc < 3) {
printk("Usage: tr <set1> <set2> <file> translate characters\n");
printk(" tr -d <set1> <file> delete characters\n");
return 1;
}
bool del = (strcmp(argv[1], "-d") == 0);
const char* set1 = del ? argv[2] : argv[1];
const char* set2 = del ? NULL : argv[2];
const char* path = del ? (argc > 3 ? argv[3] : NULL)
: (argc > 3 ? argv[3] : NULL);
if (!path) {
printk(ANSI_RED "tr: missing file operand\n" ANSI_RESET);
return 1;
}
cu_text_t t;
if (!cu_read_text(path, &t)) return 1;
size_t set1_len = strlen(set1);
size_t set2_len = set2 ? strlen(set2) : 0;
for (int i = 0; i < t.count; i++) {
for (const char* p = t.lines[i]; *p; p++) {
const char* found = strchr(set1, *p);
if (!found) {
printk("%c", *p);
continue;
}
if (del) continue;
// Short set2 repeats its final character, as GNU tr does.
size_t idx = (size_t)(found - set1);
if (idx >= set2_len) idx = set2_len ? set2_len - 1 : 0;
if (set2_len) printk("%c", set2[idx]);
}
printk("\n");
}
(void)set1_len;
cu_free_text(&t);
return 0;
}
// ===========================================================================
// rev / tac / nl
// ===========================================================================
int coreutils_rev(int argc, char** argv) {
const char* path = cu_first_operand(argc, argv);
if (!path) {
printk("Usage: rev <file> reverse each line character-wise\n");
return 1;
}
cu_text_t t;
if (!cu_read_text(path, &t)) return 1;
for (int i = 0; i < t.count; i++) {
int len = (int)strlen(t.lines[i]);
for (int c = len - 1; c >= 0; c--) printk("%c", t.lines[i][c]);
printk("\n");
}
cu_free_text(&t);
return 0;
}
int coreutils_tac(int argc, char** argv) {
const char* path = cu_first_operand(argc, argv);
if (!path) {
printk("Usage: tac <file> print lines in reverse order\n");
return 1;
}
cu_text_t t;
if (!cu_read_text(path, &t)) return 1;
for (int i = t.count - 1; i >= 0; i--) printk("%s\n", t.lines[i]);
cu_free_text(&t);
return 0;
}
int coreutils_nl(int argc, char** argv) {
const char* path = cu_first_operand(argc, argv);
if (!path) {
printk("Usage: nl [-a] <file> number lines (-a numbers blank lines too)\n");
return 1;
}
bool number_all = cu_has_flag(argc, argv, "-a");
cu_text_t t;
if (!cu_read_text(path, &t)) return 1;
int n = 1;
for (int i = 0; i < t.count; i++) {
bool blank = (t.lines[i][0] == '\0');
if (blank && !number_all) {
printk("\n");
continue;
}
printk(ANSI_BRIGHT_BLACK "%6d " ANSI_RESET "%s\n", n++, t.lines[i]);
}
cu_free_text(&t);
return 0;
}
// ===========================================================================
// seq
// ===========================================================================
int coreutils_seq(int argc, char** argv) {
if (argc < 2) {
printk("Usage: seq [first [step]] last\n");
return 1;
}
int first = 1, step = 1, last;
if (argc == 2) {
last = cu_atoi(argv[1], 1);
} else if (argc == 3) {
first = cu_atoi(argv[1], 1);
last = cu_atoi(argv[2], 1);
} else {
first = cu_atoi(argv[1], 1);
step = cu_atoi(argv[2], 1);
last = cu_atoi(argv[3], 1);
}
if (step == 0) {
printk(ANSI_RED "seq: step must not be zero\n" ANSI_RESET);
return 1;
}
int emitted = 0;
if (step > 0) {
for (int v = first; v <= last && emitted < 4096; v += step, emitted++) printk("%d\n", v);
} else {
for (int v = first; v >= last && emitted < 4096; v += step, emitted++) printk("%d\n", v);
}
return 0;
}
// ===========================================================================
// diff
// ===========================================================================
int coreutils_diff(int argc, char** argv) {
if (argc < 3) {
printk("Usage: diff <file1> <file2> line-by-line comparison\n");
return 1;
}
cu_text_t a, b;
if (!cu_read_text(argv[1], &a)) return 1;
if (!cu_read_text(argv[2], &b)) {
cu_free_text(&a);
return 1;
}
int max = (a.count > b.count) ? a.count : b.count;
int differences = 0;
for (int i = 0; i < max; i++) {
const char* la = (i < a.count) ? a.lines[i] : NULL;
const char* lb = (i < b.count) ? b.lines[i] : NULL;
if (la && lb && strcmp(la, lb) == 0) continue;
differences++;
printk(ANSI_YELLOW "@@ line %d @@\n" ANSI_RESET, i + 1);
if (la) printk(ANSI_RED "- %s\n" ANSI_RESET, la);
if (lb) printk(ANSI_GREEN "+ %s\n" ANSI_RESET, lb);
}
if (differences == 0) {
printk(ANSI_BRIGHT_GREEN "Files '%s' and '%s' are identical (%d lines)\n" ANSI_RESET,
argv[1], argv[2], a.count);
} else {
printk("\n%d differing line(s)\n", differences);
}
cu_free_text(&a);
cu_free_text(&b);
return differences ? 1 : 0;
}
// ===========================================================================
// xxd
// ===========================================================================
int coreutils_xxd(int argc, char** argv) {
const char* path = NULL;
int limit = 256;
for (int i = 1; i < argc; i++) {
if (!argv[i]) continue;
if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) limit = cu_atoi(argv[++i], 256);
else if (argv[i][0] != '-') path = argv[i];
}
if (!path) {
printk("Usage: xxd [-l <bytes>] <file> canonical hex + ASCII dump\n");
return 1;
}
vfs_node_t* node = vfs_namei(path);
if (!node) {
printk(ANSI_RED "xxd: '%s': No such file\n" ANSI_RESET, path);
return 1;
}
int fd = vfs_open(path, O_RDONLY);
if (fd < 0) {
printk(ANSI_RED "xxd: cannot open '%s'\n" ANSI_RESET, path);
return 1;
}
uint8_t row[16];
int offset = 0;
while (offset < limit) {
int want = (limit - offset > 16) ? 16 : (limit - offset);
ssize_t got = vfs_read(fd, row, (size_t)want);
if (got <= 0) break;
printk(ANSI_BRIGHT_BLACK "%08x: " ANSI_RESET, offset);
for (int i = 0; i < 16; i++) {
if (i < got) printk("%02x", row[i]);
else printk(" ");
if (i % 2) printk(" ");
}
printk(" ");
for (int i = 0; i < got; i++) {
char c = (char)row[i];
printk("%c", (c >= 32 && c < 127) ? c : '.');
}
printk("\n");
offset += (int)got;
if (got < want) break;
}
vfs_close(fd);
printk(ANSI_BRIGHT_BLACK "%d bytes dumped\n" ANSI_RESET, offset);
return 0;
}
// ===========================================================================
// du
// ===========================================================================
static uint64_t cu_du_walk(vfs_node_t* dir, const char* path, int depth, bool summarize) {
if (!dir || depth > 8) return 0;
uint64_t total = 0;
for (uint32_t i = 0; ; i++) {
vfs_dirent_t* de = dir->readdir ? dir->readdir(dir, i) : NULL;
if (!de) break;
if (strcmp(de->name, ".") == 0 || strcmp(de->name, "..") == 0) continue;
vfs_node_t* child = dir->finddir ? dir->finddir(dir, de->name) : NULL;
if (!child) continue;
char child_path[256];
if (strcmp(path, "/") == 0) snprintf(child_path, sizeof(child_path), "/%s", de->name);
else snprintf(child_path, sizeof(child_path), "%s/%s", path, de->name);
if (child->flags & FS_DIRECTORY) {
uint64_t sub = cu_du_walk(child, child_path, depth + 1, summarize);
total += sub;
if (!summarize) printk("%8llu KB %s/\n", (unsigned long long)((sub + 1023) / 1024), child_path);
} else {
total += child->length;
if (!summarize && child->length > 0) {
printk("%8llu KB %s\n",
(unsigned long long)((child->length + 1023) / 1024), child_path);
}
}
}
return total;
}
int coreutils_du(int argc, char** argv) {
bool summarize = cu_has_flag(argc, argv, "-s");
const char* path = cu_first_operand(argc, argv);
if (!path) path = vfs_getcwd();
vfs_node_t* node = vfs_namei(path);
if (!node) {
printk(ANSI_RED "du: '%s': No such file or directory\n" ANSI_RESET, path);
return 1;
}
if (!(node->flags & FS_DIRECTORY)) {
printk("%8llu KB %s\n", (unsigned long long)((node->length + 1023) / 1024), path);
return 0;
}
uint64_t total = cu_du_walk(node, path, 0, summarize);
printk(ANSI_BRIGHT_GREEN "%8llu KB %s (total)\n" ANSI_RESET,
(unsigned long long)((total + 1023) / 1024), path);
return 0;
}
// ===========================================================================
// factor
// ===========================================================================
int coreutils_factor(int argc, char** argv) {
if (argc < 2) {
printk("Usage: factor <number...> print prime factorization\n");
return 1;
}
for (int a = 1; a < argc; a++) {
long n = cu_atoi(argv[a], 0);
if (n < 2) {
printk("%ld:\n", n);
continue;
}
printk("%ld:", n);
long v = n;
for (long d = 2; d * d <= v; d++) {
while (v % d == 0) {
printk(" %ld", d);
v /= d;
}
}
if (v > 1) printk(" %ld", v);
printk("\n");
}
return 0;
}
// ===========================================================================
// sum
// ===========================================================================
int coreutils_sum(int argc, char** argv) {
const char* path = cu_first_operand(argc, argv);
if (!path) {
printk("Usage: sum <file> BSD 16-bit rotating checksum\n");
return 1;
}
int fd = vfs_open(path, O_RDONLY);
if (fd < 0) {
printk(ANSI_RED "sum: cannot open '%s'\n" ANSI_RESET, path);
return 1;
}
uint8_t buf[256];
uint32_t checksum = 0;
uint64_t bytes = 0;
ssize_t got;
while ((got = vfs_read(fd, buf, sizeof(buf))) > 0) {
for (ssize_t i = 0; i < got; i++) {
// BSD sum: rotate right one bit, then add, keeping 16 bits.
checksum = (checksum >> 1) | ((checksum & 1) << 15);
checksum = (checksum + buf[i]) & 0xFFFF;
}
bytes += (uint64_t)got;
}
vfs_close(fd);
printk("%05u %5llu %s\n", checksum,
(unsigned long long)((bytes + 1023) / 1024), path);
return 0;
}
// ===========================================================================
// truncate
// ===========================================================================
int coreutils_truncate(int argc, char** argv) {
if (argc < 3) {
printk("Usage: truncate -s <size> <file> shrink or grow a file\n");
return 1;
}
long size = -1;
const char* path = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) size = cu_atoi(argv[++i], -1);
else if (argv[i][0] != '-') path = argv[i];
}
if (!path || size < 0) {
printk(ANSI_RED "truncate: need -s <size> and a file\n" ANSI_RESET);
return 1;
}
vfs_node_t* node = vfs_namei(path);
if (!node) {
printk(ANSI_RED "truncate: '%s': No such file\n" ANSI_RESET, path);
return 1;
}
size_t old_len = node->length;
if ((size_t)size > old_len && node->write) {
// Growing: append zero bytes so the tail is well defined.
size_t pad = (size_t)size - old_len;
uint8_t zeros[64];
memset(zeros, 0, sizeof(zeros));
int fd = vfs_open(path, O_WRONLY);
if (fd >= 0) {
vfs_lseek(fd, (off_t)old_len, SEEK_SET);
while (pad > 0) {
size_t chunk = (pad > sizeof(zeros)) ? sizeof(zeros) : pad;
if (vfs_write(fd, zeros, chunk) <= 0) break;
pad -= chunk;
}
vfs_close(fd);
}
} else {
node->length = (size_t)size;
}
printk("truncate: '%s' %llu -> %llu bytes\n", path,
(unsigned long long)old_len, (unsigned long long)node->length);
return 0;
}