feat: multi-user system, bug fixes, security & performance fixes, and more
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* config.h
|
||||
* Config file manager for MontaukOS programs
|
||||
* Loads, modifies, and saves TOML config files from 0:/config/
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/toml.h>
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
namespace montauk {
|
||||
namespace config {
|
||||
|
||||
static constexpr const char* CONFIG_DIR = "0:/config";
|
||||
|
||||
// ---- Serializer (Doc -> TOML text) ----
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct Writer {
|
||||
char* buf;
|
||||
int len;
|
||||
int cap;
|
||||
|
||||
void init() {
|
||||
cap = 1024;
|
||||
buf = (char*)montauk::malloc(cap);
|
||||
len = 0;
|
||||
buf[0] = '\0';
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (buf) montauk::mfree(buf);
|
||||
buf = nullptr;
|
||||
len = 0;
|
||||
cap = 0;
|
||||
}
|
||||
|
||||
void grow(int need) {
|
||||
if (len + need < cap) return;
|
||||
int newCap = cap;
|
||||
while (newCap <= len + need) newCap *= 2;
|
||||
char* nb = (char*)montauk::malloc(newCap);
|
||||
montauk::memcpy(nb, buf, len);
|
||||
montauk::mfree(buf);
|
||||
buf = nb;
|
||||
cap = newCap;
|
||||
}
|
||||
|
||||
void put(char c) {
|
||||
grow(2);
|
||||
buf[len++] = c;
|
||||
buf[len] = '\0';
|
||||
}
|
||||
|
||||
void puts(const char* s) {
|
||||
int n = montauk::slen(s);
|
||||
grow(n + 1);
|
||||
montauk::memcpy(buf + len, s, n);
|
||||
len += n;
|
||||
buf[len] = '\0';
|
||||
}
|
||||
|
||||
void put_int(int64_t v) {
|
||||
if (v < 0) { put('-'); v = -v; }
|
||||
if (v == 0) { put('0'); return; }
|
||||
char tmp[24];
|
||||
int n = 0;
|
||||
while (v > 0) { tmp[n++] = '0' + (v % 10); v /= 10; }
|
||||
for (int i = n - 1; i >= 0; i--) put(tmp[i]);
|
||||
}
|
||||
|
||||
// Write a TOML-escaped string value
|
||||
void put_string(const char* s) {
|
||||
put('"');
|
||||
while (*s) {
|
||||
switch (*s) {
|
||||
case '"': puts("\\\""); break;
|
||||
case '\\': puts("\\\\"); break;
|
||||
case '\n': puts("\\n"); break;
|
||||
case '\t': puts("\\t"); break;
|
||||
case '\r': puts("\\r"); break;
|
||||
default: put(*s); break;
|
||||
}
|
||||
s++;
|
||||
}
|
||||
put('"');
|
||||
}
|
||||
|
||||
void write_value(toml::Value* v) {
|
||||
switch (v->type) {
|
||||
case toml::Type::String:
|
||||
put_string(v->str);
|
||||
break;
|
||||
case toml::Type::Int:
|
||||
put_int(v->ival);
|
||||
break;
|
||||
case toml::Type::Bool:
|
||||
puts(v->bval ? "true" : "false");
|
||||
break;
|
||||
case toml::Type::Array: {
|
||||
put('[');
|
||||
for (int i = 0; i < v->array.count; i++) {
|
||||
if (i > 0) puts(", ");
|
||||
write_value(v->array.items[i]);
|
||||
}
|
||||
put(']');
|
||||
break;
|
||||
}
|
||||
case toml::Type::Table:
|
||||
// Inline tables are not re-serialized here;
|
||||
// their entries are flattened in the doc
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Extract the table prefix from a dotted key (everything before last dot)
|
||||
// Returns length of prefix, or 0 if no dot
|
||||
inline int key_table(const char* key, char* out, int outSz) {
|
||||
int lastDot = -1;
|
||||
int n = montauk::slen(key);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (key[i] == '.') lastDot = i;
|
||||
}
|
||||
if (lastDot <= 0) { out[0] = '\0'; return 0; }
|
||||
int cp = lastDot < outSz - 1 ? lastDot : outSz - 1;
|
||||
montauk::memcpy(out, key, cp);
|
||||
out[cp] = '\0';
|
||||
return cp;
|
||||
}
|
||||
|
||||
// Extract the bare key (after last dot)
|
||||
inline const char* key_bare(const char* key) {
|
||||
const char* last = key;
|
||||
while (*key) {
|
||||
if (*key == '.') last = key + 1;
|
||||
key++;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Serialize a Doc back to TOML text. Caller must montauk::mfree() the result.
|
||||
inline char* serialize(toml::Doc* doc) {
|
||||
detail::Writer w;
|
||||
w.init();
|
||||
|
||||
char currentTable[256] = {};
|
||||
|
||||
for (int i = 0; i < doc->entries.count; i++) {
|
||||
auto* v = doc->entries.items[i];
|
||||
if (!v->key) continue;
|
||||
|
||||
// Skip Table entries that only serve as section markers —
|
||||
// we emit [table] headers based on the keys of actual values
|
||||
if (v->type == toml::Type::Table && v->array.count == 0)
|
||||
continue;
|
||||
|
||||
// Determine table prefix for this key
|
||||
char table[256];
|
||||
detail::key_table(v->key, table, sizeof(table));
|
||||
|
||||
// Emit [table] header if the section changed
|
||||
if (!montauk::streq(table, currentTable)) {
|
||||
montauk::strcpy(currentTable, table);
|
||||
if (table[0]) {
|
||||
if (w.len > 0) w.put('\n');
|
||||
w.put('[');
|
||||
w.puts(table);
|
||||
w.puts("]\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Emit key = value
|
||||
w.puts(detail::key_bare(v->key));
|
||||
w.puts(" = ");
|
||||
w.write_value(v);
|
||||
w.put('\n');
|
||||
}
|
||||
|
||||
return w.buf;
|
||||
}
|
||||
|
||||
// ---- File operations ----
|
||||
|
||||
// Ensure the config directory exists
|
||||
inline void ensure_dir() {
|
||||
montauk::fmkdir(CONFIG_DIR);
|
||||
}
|
||||
|
||||
// Build full path: "0:/config/<name>.toml"
|
||||
inline void build_path(char* out, int outSz, const char* name) {
|
||||
int p = 0;
|
||||
const char* dir = CONFIG_DIR;
|
||||
while (*dir && p < outSz - 2) out[p++] = *dir++;
|
||||
out[p++] = '/';
|
||||
while (*name && p < outSz - 6) out[p++] = *name++;
|
||||
// Append ".toml"
|
||||
const char* ext = ".toml";
|
||||
while (*ext && p < outSz - 1) out[p++] = *ext++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// Load a config file by name (without extension).
|
||||
// Returns an initialized Doc (empty if file doesn't exist).
|
||||
inline toml::Doc load(const char* name) {
|
||||
char path[128];
|
||||
build_path(path, sizeof(path), name);
|
||||
|
||||
int handle = montauk::open(path);
|
||||
if (handle < 0) {
|
||||
toml::Doc doc;
|
||||
doc.init();
|
||||
return doc;
|
||||
}
|
||||
|
||||
uint64_t size = montauk::getsize(handle);
|
||||
if (size == 0) {
|
||||
montauk::close(handle);
|
||||
toml::Doc doc;
|
||||
doc.init();
|
||||
return doc;
|
||||
}
|
||||
|
||||
char* text = (char*)montauk::malloc(size + 1);
|
||||
montauk::read(handle, (uint8_t*)text, 0, size);
|
||||
montauk::close(handle);
|
||||
text[size] = '\0';
|
||||
|
||||
toml::Doc doc = toml::parse(text);
|
||||
montauk::mfree(text);
|
||||
return doc;
|
||||
}
|
||||
|
||||
// Save a Doc to disk as a TOML file.
|
||||
// Creates the file if it doesn't exist.
|
||||
// Returns 0 on success, negative on error.
|
||||
inline int save(const char* name, toml::Doc* doc) {
|
||||
ensure_dir();
|
||||
|
||||
char path[128];
|
||||
build_path(path, sizeof(path), name);
|
||||
|
||||
char* text = serialize(doc);
|
||||
int textLen = montauk::slen(text);
|
||||
|
||||
// Delete and recreate to ensure file is exactly the right size
|
||||
// (no ftruncate syscall available, so this avoids stale trailing data)
|
||||
montauk::fdelete(path);
|
||||
int handle = montauk::fcreate(path);
|
||||
if (handle < 0) {
|
||||
montauk::mfree(text);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = montauk::fwrite(handle, (const uint8_t*)text, 0, textLen);
|
||||
montauk::close(handle);
|
||||
montauk::mfree(text);
|
||||
return ret < 0 ? ret : 0;
|
||||
}
|
||||
|
||||
// ---- Per-user config ----
|
||||
|
||||
// Build path: "0:/users/<username>/config/<name>.toml"
|
||||
inline void build_user_path(char* out, int outSz, const char* username, const char* name) {
|
||||
int p = 0;
|
||||
const char* prefix = "0:/users/";
|
||||
while (*prefix && p < outSz - 2) out[p++] = *prefix++;
|
||||
while (*username && p < outSz - 2) out[p++] = *username++;
|
||||
const char* mid = "/config/";
|
||||
while (*mid && p < outSz - 2) out[p++] = *mid++;
|
||||
while (*name && p < outSz - 6) out[p++] = *name++;
|
||||
const char* ext = ".toml";
|
||||
while (*ext && p < outSz - 1) out[p++] = *ext++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// Ensure per-user config directory exists
|
||||
inline void ensure_user_dir(const char* username) {
|
||||
char dir[128];
|
||||
int p = 0;
|
||||
const char* prefix = "0:/users/";
|
||||
while (*prefix && p < 126) dir[p++] = *prefix++;
|
||||
while (*username && p < 126) dir[p++] = *username++;
|
||||
dir[p] = '\0';
|
||||
montauk::fmkdir(dir);
|
||||
|
||||
const char* suffix = "/config";
|
||||
while (*suffix && p < 126) dir[p++] = *suffix++;
|
||||
dir[p] = '\0';
|
||||
montauk::fmkdir(dir);
|
||||
}
|
||||
|
||||
// Load a per-user config file
|
||||
inline toml::Doc load_user(const char* username, const char* name) {
|
||||
char path[192];
|
||||
build_user_path(path, sizeof(path), username, name);
|
||||
|
||||
int handle = montauk::open(path);
|
||||
if (handle < 0) {
|
||||
toml::Doc doc;
|
||||
doc.init();
|
||||
return doc;
|
||||
}
|
||||
|
||||
uint64_t size = montauk::getsize(handle);
|
||||
if (size == 0) {
|
||||
montauk::close(handle);
|
||||
toml::Doc doc;
|
||||
doc.init();
|
||||
return doc;
|
||||
}
|
||||
|
||||
char* text = (char*)montauk::malloc(size + 1);
|
||||
montauk::read(handle, (uint8_t*)text, 0, size);
|
||||
montauk::close(handle);
|
||||
text[size] = '\0';
|
||||
|
||||
toml::Doc doc = toml::parse(text);
|
||||
montauk::mfree(text);
|
||||
return doc;
|
||||
}
|
||||
|
||||
// Save a per-user config file
|
||||
inline int save_user(const char* username, const char* name, toml::Doc* doc) {
|
||||
ensure_user_dir(username);
|
||||
|
||||
char path[192];
|
||||
build_user_path(path, sizeof(path), username, name);
|
||||
|
||||
char* text = serialize(doc);
|
||||
int textLen = montauk::slen(text);
|
||||
|
||||
montauk::fdelete(path);
|
||||
int handle = montauk::fcreate(path);
|
||||
if (handle < 0) {
|
||||
montauk::mfree(text);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = montauk::fwrite(handle, (const uint8_t*)text, 0, textLen);
|
||||
montauk::close(handle);
|
||||
montauk::mfree(text);
|
||||
return ret < 0 ? ret : 0;
|
||||
}
|
||||
|
||||
// Delete a config file. Returns 0 on success.
|
||||
inline int remove(const char* name) {
|
||||
char path[128];
|
||||
build_path(path, sizeof(path), name);
|
||||
return montauk::fdelete(path);
|
||||
}
|
||||
|
||||
// ---- In-place modification helpers ----
|
||||
|
||||
// Free any dynamically allocated data owned by a value (without freeing key)
|
||||
namespace detail {
|
||||
inline void free_value_data(toml::Value* v) {
|
||||
if (v->type == toml::Type::String && v->str)
|
||||
montauk::mfree(v->str);
|
||||
else if (v->type == toml::Type::Array || v->type == toml::Type::Table) {
|
||||
for (int i = 0; i < v->array.count; i++) {
|
||||
if (v->array.items[i]) v->array.items[i]->destroy();
|
||||
}
|
||||
if (v->array.items) montauk::mfree(v->array.items);
|
||||
v->array.items = nullptr;
|
||||
v->array.count = 0;
|
||||
v->array.cap = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set or overwrite a string value in the doc.
|
||||
inline void set_string(toml::Doc* doc, const char* key, const char* val) {
|
||||
auto* existing = doc->get(key);
|
||||
if (existing) {
|
||||
detail::free_value_data(existing);
|
||||
existing->type = toml::Type::String;
|
||||
existing->str = toml::Value::dup(val);
|
||||
} else {
|
||||
doc->entries.push(toml::Value::make_string(key, val, montauk::slen(val)));
|
||||
}
|
||||
}
|
||||
|
||||
// Set or overwrite an integer value in the doc.
|
||||
inline void set_int(toml::Doc* doc, const char* key, int64_t val) {
|
||||
auto* existing = doc->get(key);
|
||||
if (existing) {
|
||||
detail::free_value_data(existing);
|
||||
existing->type = toml::Type::Int;
|
||||
existing->ival = val;
|
||||
} else {
|
||||
doc->entries.push(toml::Value::make_int(key, val));
|
||||
}
|
||||
}
|
||||
|
||||
// Set or overwrite a boolean value in the doc.
|
||||
inline void set_bool(toml::Doc* doc, const char* key, bool val) {
|
||||
auto* existing = doc->get(key);
|
||||
if (existing) {
|
||||
detail::free_value_data(existing);
|
||||
existing->type = toml::Type::Bool;
|
||||
existing->bval = val;
|
||||
} else {
|
||||
doc->entries.push(toml::Value::make_bool(key, val));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a key from the doc.
|
||||
inline bool unset(toml::Doc* doc, const char* key) {
|
||||
for (int i = 0; i < doc->entries.count; i++) {
|
||||
auto* v = doc->entries.items[i];
|
||||
if (v->key && montauk::streq(v->key, key)) {
|
||||
v->destroy();
|
||||
// Shift remaining entries down
|
||||
for (int j = i; j < doc->entries.count - 1; j++)
|
||||
doc->entries.items[j] = doc->entries.items[j + 1];
|
||||
doc->entries.count--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace config
|
||||
} // namespace montauk
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* heap.h
|
||||
* Userspace heap allocator for MontaukOS programs
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
|
||||
namespace montauk {
|
||||
namespace heap_detail {
|
||||
|
||||
static constexpr uint64_t HEADER_MAGIC = 0x5A484541; // "ZHEA"
|
||||
static constexpr uint64_t FREED_MAGIC = 0xDEADFEEE;
|
||||
|
||||
struct Header {
|
||||
uint64_t magic;
|
||||
uint64_t size; // user-requested size
|
||||
} __attribute__((packed));
|
||||
|
||||
struct FreeNode {
|
||||
uint64_t size; // total size of this free block (including node)
|
||||
FreeNode* next;
|
||||
};
|
||||
|
||||
// Segregated free lists: power-of-2 size classes for blocks <= 4096 bytes.
|
||||
// Blocks larger than 4096 go to the overflow list.
|
||||
static constexpr int NUM_BUCKETS = 8;
|
||||
static constexpr uint64_t BUCKET_SIZES[NUM_BUCKETS] = {
|
||||
32, 64, 128, 256, 512, 1024, 2048, 4096
|
||||
};
|
||||
|
||||
// Per-process heap state — must be `inline` (not `static`) so that all
|
||||
// translation units in a multi-TU program share a single heap.
|
||||
inline FreeNode* g_buckets[NUM_BUCKETS] = {};
|
||||
inline FreeNode g_overflow{0, nullptr};
|
||||
inline bool g_initialized = false;
|
||||
|
||||
static inline Header* get_header(void* block) {
|
||||
return (Header*)((uint8_t*)block - sizeof(Header));
|
||||
}
|
||||
|
||||
// Determine which bucket a block size belongs to, or -1 for overflow
|
||||
static inline int bucket_index(uint64_t blockSize) {
|
||||
if (blockSize <= 32) return 0;
|
||||
if (blockSize <= 64) return 1;
|
||||
if (blockSize <= 128) return 2;
|
||||
if (blockSize <= 256) return 3;
|
||||
if (blockSize <= 512) return 4;
|
||||
if (blockSize <= 1024) return 5;
|
||||
if (blockSize <= 2048) return 6;
|
||||
if (blockSize <= 4096) return 7;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Insert into overflow list (sorted by address, with adjacent-block coalescing)
|
||||
static inline void insert_overflow(void* ptr, uint64_t size) {
|
||||
auto* node = (FreeNode*)ptr;
|
||||
node->size = size;
|
||||
|
||||
FreeNode* prev = &g_overflow;
|
||||
FreeNode* cur = g_overflow.next;
|
||||
while (cur != nullptr && cur < node) {
|
||||
prev = cur;
|
||||
cur = cur->next;
|
||||
}
|
||||
|
||||
bool merged_prev = false;
|
||||
if (prev != &g_overflow &&
|
||||
(uint8_t*)prev + prev->size == (uint8_t*)node) {
|
||||
prev->size += size;
|
||||
node = prev;
|
||||
merged_prev = true;
|
||||
}
|
||||
|
||||
if (cur != nullptr &&
|
||||
(uint8_t*)node + node->size == (uint8_t*)cur) {
|
||||
node->size += cur->size;
|
||||
node->next = cur->next;
|
||||
if (!merged_prev) prev->next = node;
|
||||
} else if (!merged_prev) {
|
||||
node->next = cur;
|
||||
prev->next = node;
|
||||
}
|
||||
}
|
||||
|
||||
// Take a block of at least `needed` bytes from the overflow list.
|
||||
// Splits remainder back into overflow if worthwhile.
|
||||
static inline void* take_from_overflow(uint64_t needed) {
|
||||
FreeNode* prev = &g_overflow;
|
||||
FreeNode* cur = g_overflow.next;
|
||||
|
||||
while (cur != nullptr) {
|
||||
if (cur->size >= needed) {
|
||||
uint64_t blockSize = cur->size;
|
||||
prev->next = cur->next;
|
||||
|
||||
if (blockSize > needed + sizeof(FreeNode) + 16) {
|
||||
insert_overflow((uint8_t*)cur + needed, blockSize - needed);
|
||||
}
|
||||
return (void*)cur;
|
||||
}
|
||||
prev = cur;
|
||||
cur = cur->next;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static inline bool grow(uint64_t bytes) {
|
||||
uint64_t pages = (bytes + 0xFFF) / 0x1000;
|
||||
if (pages < 4) pages = 4;
|
||||
|
||||
void* mem = montauk::alloc(pages * 0x1000);
|
||||
if (mem == nullptr) return false;
|
||||
insert_overflow(mem, pages * 0x1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refill a small-block bucket by carving a page-sized chunk from overflow
|
||||
static inline bool refill_bucket(int idx) {
|
||||
uint64_t bsize = BUCKET_SIZES[idx];
|
||||
uint64_t chunk = (bsize < 4096) ? 4096 : bsize;
|
||||
|
||||
void* block = take_from_overflow(chunk);
|
||||
if (block == nullptr) {
|
||||
if (!grow(chunk)) return false;
|
||||
block = take_from_overflow(chunk);
|
||||
if (block == nullptr) return false;
|
||||
}
|
||||
|
||||
uint64_t count = chunk / bsize;
|
||||
for (uint64_t i = 0; i < count; i++) {
|
||||
auto* node = (FreeNode*)((uint8_t*)block + i * bsize);
|
||||
node->size = bsize;
|
||||
node->next = g_buckets[idx];
|
||||
g_buckets[idx] = node;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace heap_detail
|
||||
|
||||
// ---- Public API ----
|
||||
|
||||
inline void* malloc(uint64_t size) {
|
||||
using namespace heap_detail;
|
||||
|
||||
if (!g_initialized) {
|
||||
grow(16 * 0x1000); // seed with 64 KiB
|
||||
g_initialized = true;
|
||||
}
|
||||
|
||||
// Guard against overflow: size + Header must not wrap
|
||||
if (size > UINT64_MAX - sizeof(Header) - 15)
|
||||
return nullptr;
|
||||
|
||||
uint64_t needed = size + sizeof(Header);
|
||||
needed = (needed + 15) & ~15ULL;
|
||||
|
||||
int idx = bucket_index(needed);
|
||||
|
||||
if (idx >= 0) {
|
||||
// Small allocation — use segregated bucket (O(1))
|
||||
if (g_buckets[idx] == nullptr && !refill_bucket(idx))
|
||||
return nullptr;
|
||||
|
||||
FreeNode* node = g_buckets[idx];
|
||||
g_buckets[idx] = node->next;
|
||||
|
||||
Header* header = (Header*)node;
|
||||
header->magic = HEADER_MAGIC;
|
||||
header->size = size;
|
||||
return (void*)((uint8_t*)header + sizeof(Header));
|
||||
}
|
||||
|
||||
// Large allocation — search overflow list
|
||||
void* block = take_from_overflow(needed);
|
||||
if (block == nullptr) {
|
||||
if (!grow(needed)) return nullptr;
|
||||
block = take_from_overflow(needed);
|
||||
if (block == nullptr) return nullptr;
|
||||
}
|
||||
|
||||
Header* header = (Header*)block;
|
||||
header->magic = HEADER_MAGIC;
|
||||
header->size = size;
|
||||
return (void*)((uint8_t*)header + sizeof(Header));
|
||||
}
|
||||
|
||||
inline void mfree(void* ptr) {
|
||||
using namespace heap_detail;
|
||||
|
||||
if (ptr == nullptr) return;
|
||||
|
||||
Header* header = get_header(ptr);
|
||||
|
||||
if (header->magic == FREED_MAGIC) return; // double-free
|
||||
if (header->magic != HEADER_MAGIC) return; // corrupt
|
||||
header->magic = FREED_MAGIC;
|
||||
|
||||
uint64_t blockSize = header->size + sizeof(Header);
|
||||
blockSize = (blockSize + 15) & ~15ULL;
|
||||
|
||||
int idx = bucket_index(blockSize);
|
||||
|
||||
if (idx >= 0) {
|
||||
// Small block — push onto bucket (O(1))
|
||||
auto* node = (FreeNode*)header;
|
||||
node->size = BUCKET_SIZES[idx];
|
||||
node->next = g_buckets[idx];
|
||||
g_buckets[idx] = node;
|
||||
} else {
|
||||
// Large block — sorted insert with coalescing
|
||||
insert_overflow((void*)header, blockSize);
|
||||
}
|
||||
}
|
||||
|
||||
inline void* realloc(void* ptr, uint64_t size) {
|
||||
if (ptr == nullptr) return malloc(size);
|
||||
|
||||
auto* header = heap_detail::get_header(ptr);
|
||||
uint64_t old = header->size;
|
||||
|
||||
// Compute actual block size (accounting for bucket rounding)
|
||||
uint64_t oldBlock = (old + sizeof(heap_detail::Header) + 15) & ~15ULL;
|
||||
int idx = heap_detail::bucket_index(oldBlock);
|
||||
if (idx >= 0) oldBlock = heap_detail::BUCKET_SIZES[idx];
|
||||
|
||||
uint64_t newNeed = (size + sizeof(heap_detail::Header) + 15) & ~15ULL;
|
||||
if (newNeed <= oldBlock) {
|
||||
header->size = size;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void* newBlock = malloc(size);
|
||||
if (newBlock == nullptr) return nullptr;
|
||||
|
||||
uint64_t copySize = (old < size) ? old : size;
|
||||
memcpy(newBlock, ptr, copySize);
|
||||
|
||||
mfree(ptr);
|
||||
return newBlock;
|
||||
}
|
||||
|
||||
} // namespace montauk
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* string.h
|
||||
* Common string and memory utility functions for MontaukOS programs
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace montauk {
|
||||
|
||||
inline int slen(const char* s) {
|
||||
int n = 0;
|
||||
while (s[n]) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
inline bool streq(const char* a, const char* b) {
|
||||
while (*a && *b) {
|
||||
if (*a != *b) return false;
|
||||
a++; b++;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
inline bool starts_with(const char* str, const char* prefix) {
|
||||
while (*prefix) {
|
||||
if (*str != *prefix) return false;
|
||||
str++; prefix++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline const char* skip_spaces(const char* s) {
|
||||
while (*s == ' ') s++;
|
||||
return s;
|
||||
}
|
||||
|
||||
inline void memcpy(void* dst, const void* src, uint64_t n) {
|
||||
auto* d = (uint8_t*)dst;
|
||||
auto* s = (const uint8_t*)src;
|
||||
|
||||
// Byte copy until 8-byte aligned
|
||||
while (n && ((uint64_t)d & 7)) { *d++ = *s++; n--; }
|
||||
|
||||
// Bulk 8-byte copy
|
||||
auto* d8 = (uint64_t*)d;
|
||||
auto* s8 = (const uint64_t*)s;
|
||||
uint64_t words = n / 8;
|
||||
for (uint64_t i = 0; i < words; i++) d8[i] = s8[i];
|
||||
|
||||
// Remainder
|
||||
d = (uint8_t*)(d8 + words);
|
||||
s = (const uint8_t*)(s8 + words);
|
||||
for (uint64_t i = 0; i < (n & 7); i++) d[i] = s[i];
|
||||
}
|
||||
|
||||
inline void memmove(void* dst, const void* src, uint64_t n) {
|
||||
auto* d = (uint8_t*)dst;
|
||||
auto* s = (const uint8_t*)src;
|
||||
if (d < s || d >= s + n) {
|
||||
memcpy(dst, src, n);
|
||||
} else {
|
||||
// Backward copy — bulk 8 bytes at a time from end
|
||||
d += n; s += n;
|
||||
while (n && ((uint64_t)d & 7)) { *--d = *--s; n--; }
|
||||
auto* d8 = (uint64_t*)d;
|
||||
auto* s8 = (const uint64_t*)s;
|
||||
uint64_t words = n / 8;
|
||||
for (uint64_t i = 1; i <= words; i++) d8[-i] = s8[-i];
|
||||
d = (uint8_t*)(d8 - words);
|
||||
s = (const uint8_t*)(s8 - words);
|
||||
for (uint64_t i = 1; i <= (n & 7); i++) d[-i] = s[-i];
|
||||
}
|
||||
}
|
||||
|
||||
inline void memset(void* dst, int val, uint64_t n) {
|
||||
auto* d = (uint8_t*)dst;
|
||||
uint8_t v = (uint8_t)val;
|
||||
|
||||
// Byte fill until 8-byte aligned
|
||||
while (n && ((uint64_t)d & 7)) { *d++ = v; n--; }
|
||||
|
||||
// Bulk 8-byte fill
|
||||
uint64_t v8 = v;
|
||||
v8 |= v8 << 8; v8 |= v8 << 16; v8 |= v8 << 32;
|
||||
auto* d8 = (uint64_t*)d;
|
||||
uint64_t words = n / 8;
|
||||
for (uint64_t i = 0; i < words; i++) d8[i] = v8;
|
||||
|
||||
// Remainder
|
||||
d = (uint8_t*)(d8 + words);
|
||||
for (uint64_t i = 0; i < (n & 7); i++) d[i] = v;
|
||||
}
|
||||
|
||||
inline void strcpy(char* dst, const char* src) {
|
||||
while (*src) *dst++ = *src++;
|
||||
*dst = '\0';
|
||||
}
|
||||
|
||||
inline void strncpy(char* dst, const char* src, int max) {
|
||||
if (max <= 0) return;
|
||||
int i = 0;
|
||||
while (src[i] && i < max - 1) { dst[i] = src[i]; i++; }
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* syscall.h
|
||||
* MontaukOS program-side syscall wrappers using SYSCALL instruction
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Api/Syscall.hpp>
|
||||
|
||||
namespace montauk {
|
||||
|
||||
// ---- Raw SYSCALL wrappers ----
|
||||
|
||||
// The SYSCALL handler does not restore RDI, RSI, RDX, R10, R8, R9
|
||||
// (they are skipped on the return path). We move arguments into the
|
||||
// correct registers inside the asm block and list ALL argument
|
||||
// registers in the clobber list. This guarantees the compiler
|
||||
// reloads every argument on each call — GCC cannot optimise away
|
||||
// clobbers, unlike "+r" outputs whose dead values it may discard.
|
||||
|
||||
inline int64_t syscall0(uint64_t nr) {
|
||||
int64_t ret;
|
||||
asm volatile("syscall" : "=a"(ret) : "a"(nr)
|
||||
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline int64_t syscall1(uint64_t nr, uint64_t a1) {
|
||||
int64_t ret;
|
||||
asm volatile(
|
||||
"mov %[a1], %%rdi\n\t"
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(nr), [a1] "r"(a1)
|
||||
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline int64_t syscall2(uint64_t nr, uint64_t a1, uint64_t a2) {
|
||||
int64_t ret;
|
||||
asm volatile(
|
||||
"mov %[a1], %%rdi\n\t"
|
||||
"mov %[a2], %%rsi\n\t"
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(nr), [a1] "r"(a1), [a2] "r"(a2)
|
||||
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline int64_t syscall3(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3) {
|
||||
int64_t ret;
|
||||
asm volatile(
|
||||
"mov %[a1], %%rdi\n\t"
|
||||
"mov %[a2], %%rsi\n\t"
|
||||
"mov %[a3], %%rdx\n\t"
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3)
|
||||
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline int64_t syscall4(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4) {
|
||||
int64_t ret;
|
||||
asm volatile(
|
||||
"mov %[a1], %%rdi\n\t"
|
||||
"mov %[a2], %%rsi\n\t"
|
||||
"mov %[a3], %%rdx\n\t"
|
||||
"mov %[a4], %%r10\n\t"
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4)
|
||||
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline int64_t syscall5(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) {
|
||||
int64_t ret;
|
||||
asm volatile(
|
||||
"mov %[a1], %%rdi\n\t"
|
||||
"mov %[a2], %%rsi\n\t"
|
||||
"mov %[a3], %%rdx\n\t"
|
||||
"mov %[a4], %%r10\n\t"
|
||||
"mov %[a5], %%r8\n\t"
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4), [a5] "r"(a5)
|
||||
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline int64_t syscall6(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5, uint64_t a6) {
|
||||
int64_t ret;
|
||||
asm volatile(
|
||||
"mov %[a1], %%rdi\n\t"
|
||||
"mov %[a2], %%rsi\n\t"
|
||||
"mov %[a3], %%rdx\n\t"
|
||||
"mov %[a4], %%r10\n\t"
|
||||
"mov %[a5], %%r8\n\t"
|
||||
"mov %[a6], %%r9\n\t"
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4), [a5] "r"(a5), [a6] "r"(a6)
|
||||
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ---- Typed wrappers ----
|
||||
|
||||
// Process
|
||||
[[noreturn]] inline void exit(int code = 0) {
|
||||
syscall1(Montauk::SYS_EXIT, (uint64_t)code);
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
inline void yield() { syscall0(Montauk::SYS_YIELD); }
|
||||
inline void sleep_ms(uint64_t ms) { syscall1(Montauk::SYS_SLEEP_MS, ms); }
|
||||
inline int getpid() { return (int)syscall0(Montauk::SYS_GETPID); }
|
||||
inline int spawn(const char* path, const char* args = nullptr) {
|
||||
return (int)syscall2(Montauk::SYS_SPAWN, (uint64_t)path, (uint64_t)args);
|
||||
}
|
||||
|
||||
// Console
|
||||
inline void print(const char* text) { syscall1(Montauk::SYS_PRINT, (uint64_t)text); }
|
||||
inline void putchar(char c) { syscall1(Montauk::SYS_PUTCHAR, (uint64_t)c); }
|
||||
|
||||
// File I/O
|
||||
inline int open(const char* path) { return (int)syscall1(Montauk::SYS_OPEN, (uint64_t)path); }
|
||||
inline int read(int handle, uint8_t* buf, uint64_t off, uint64_t size) {
|
||||
return (int)syscall4(Montauk::SYS_READ, (uint64_t)handle, (uint64_t)buf, off, size);
|
||||
}
|
||||
inline uint64_t getsize(int handle) { return (uint64_t)syscall1(Montauk::SYS_GETSIZE, (uint64_t)handle); }
|
||||
inline void close(int handle) { syscall1(Montauk::SYS_CLOSE, (uint64_t)handle); }
|
||||
inline int readdir(const char* path, const char** names, int max) {
|
||||
return (int)syscall3(Montauk::SYS_READDIR, (uint64_t)path, (uint64_t)names, (uint64_t)max);
|
||||
}
|
||||
|
||||
// File write/create
|
||||
inline int fwrite(int handle, const uint8_t* buf, uint64_t off, uint64_t size) {
|
||||
return (int)syscall4(Montauk::SYS_FWRITE, (uint64_t)handle, (uint64_t)buf, off, size);
|
||||
}
|
||||
inline int fcreate(const char* path) {
|
||||
return (int)syscall1(Montauk::SYS_FCREATE, (uint64_t)path);
|
||||
}
|
||||
inline int fdelete(const char* path) {
|
||||
return (int)syscall1(Montauk::SYS_FDELETE, (uint64_t)path);
|
||||
}
|
||||
inline int fmkdir(const char* path) {
|
||||
return (int)syscall1(Montauk::SYS_FMKDIR, (uint64_t)path);
|
||||
}
|
||||
inline int drivelist(int* outDrives, int max) {
|
||||
return (int)syscall2(Montauk::SYS_DRIVELIST, (uint64_t)outDrives, (uint64_t)max);
|
||||
}
|
||||
|
||||
// Memory
|
||||
inline void* alloc(uint64_t size) { return (void*)syscall1(Montauk::SYS_ALLOC, size); }
|
||||
inline void free(void* ptr) { syscall1(Montauk::SYS_FREE, (uint64_t)ptr); }
|
||||
|
||||
// Timekeeping
|
||||
inline uint64_t get_ticks() { return (uint64_t)syscall0(Montauk::SYS_GETTICKS); }
|
||||
inline uint64_t get_milliseconds() { return (uint64_t)syscall0(Montauk::SYS_GETMILLISECONDS); }
|
||||
|
||||
// System
|
||||
inline void get_info(Montauk::SysInfo* info) { syscall1(Montauk::SYS_GETINFO, (uint64_t)info); }
|
||||
|
||||
// Keyboard
|
||||
inline bool is_key_available() { return (bool)syscall0(Montauk::SYS_ISKEYAVAILABLE); }
|
||||
inline void getkey(Montauk::KeyEvent* out) { syscall1(Montauk::SYS_GETKEY, (uint64_t)out); }
|
||||
inline char getchar() { return (char)syscall0(Montauk::SYS_GETCHAR); }
|
||||
|
||||
// Networking
|
||||
inline int32_t ping(uint32_t ip, uint32_t timeoutMs = 3000) {
|
||||
return (int32_t)syscall2(Montauk::SYS_PING, (uint64_t)ip, (uint64_t)timeoutMs);
|
||||
}
|
||||
|
||||
// DNS resolve: returns IP in network byte order, or 0 on failure
|
||||
inline uint32_t resolve(const char* hostname) {
|
||||
return (uint32_t)syscall1(Montauk::SYS_RESOLVE, (uint64_t)hostname);
|
||||
}
|
||||
|
||||
// Network configuration
|
||||
inline void get_netcfg(Montauk::NetCfg* out) { syscall1(Montauk::SYS_GETNETCFG, (uint64_t)out); }
|
||||
inline int set_netcfg(const Montauk::NetCfg* cfg) { return (int)syscall1(Montauk::SYS_SETNETCFG, (uint64_t)cfg); }
|
||||
|
||||
// Sockets
|
||||
inline int socket(int type) {
|
||||
return (int)syscall1(Montauk::SYS_SOCKET, (uint64_t)type);
|
||||
}
|
||||
inline int connect(int fd, uint32_t ip, uint16_t port) {
|
||||
return (int)syscall3(Montauk::SYS_CONNECT, (uint64_t)fd, (uint64_t)ip, (uint64_t)port);
|
||||
}
|
||||
inline int bind(int fd, uint16_t port) {
|
||||
return (int)syscall2(Montauk::SYS_BIND, (uint64_t)fd, (uint64_t)port);
|
||||
}
|
||||
inline int listen(int fd) {
|
||||
return (int)syscall1(Montauk::SYS_LISTEN, (uint64_t)fd);
|
||||
}
|
||||
inline int accept(int fd) {
|
||||
return (int)syscall1(Montauk::SYS_ACCEPT, (uint64_t)fd);
|
||||
}
|
||||
inline int send(int fd, const void* data, uint32_t len) {
|
||||
return (int)syscall3(Montauk::SYS_SEND, (uint64_t)fd, (uint64_t)data, (uint64_t)len);
|
||||
}
|
||||
inline int recv(int fd, void* buf, uint32_t maxLen) {
|
||||
return (int)syscall3(Montauk::SYS_RECV, (uint64_t)fd, (uint64_t)buf, (uint64_t)maxLen);
|
||||
}
|
||||
inline int closesocket(int fd) {
|
||||
return (int)syscall1(Montauk::SYS_CLOSESOCK, (uint64_t)fd);
|
||||
}
|
||||
inline int sendto(int fd, const void* data, uint32_t len, uint32_t destIp, uint16_t destPort) {
|
||||
return (int)syscall5(Montauk::SYS_SENDTO, (uint64_t)fd, (uint64_t)data,
|
||||
(uint64_t)len, (uint64_t)destIp, (uint64_t)destPort);
|
||||
}
|
||||
inline int recvfrom(int fd, void* buf, uint32_t maxLen, uint32_t* srcIp, uint16_t* srcPort) {
|
||||
return (int)syscall5(Montauk::SYS_RECVFROM, (uint64_t)fd, (uint64_t)buf,
|
||||
(uint64_t)maxLen, (uint64_t)srcIp, (uint64_t)srcPort);
|
||||
}
|
||||
|
||||
// Process management
|
||||
inline void waitpid(int pid) { syscall1(Montauk::SYS_WAITPID, (uint64_t)pid); }
|
||||
|
||||
// Framebuffer
|
||||
inline void fb_info(Montauk::FbInfo* info) { syscall1(Montauk::SYS_FBINFO, (uint64_t)info); }
|
||||
inline void* fb_map() { return (void*)syscall0(Montauk::SYS_FBMAP); }
|
||||
|
||||
// Arguments
|
||||
inline int getargs(char* buf, uint64_t maxLen) {
|
||||
return (int)syscall2(Montauk::SYS_GETARGS, (uint64_t)buf, maxLen);
|
||||
}
|
||||
|
||||
// Terminal
|
||||
inline void termsize(int* cols, int* rows) {
|
||||
uint64_t r = (uint64_t)syscall0(Montauk::SYS_TERMSIZE);
|
||||
if (cols) *cols = (int)(r & 0xFFFFFFFF);
|
||||
if (rows) *rows = (int)(r >> 32);
|
||||
}
|
||||
|
||||
inline void termscale(int scale_x, int scale_y) {
|
||||
syscall2(Montauk::SYS_TERMSCALE, (uint64_t)scale_x, (uint64_t)scale_y);
|
||||
}
|
||||
|
||||
inline void get_termscale(int* scale_x, int* scale_y) {
|
||||
uint64_t r = (uint64_t)syscall2(Montauk::SYS_TERMSCALE, 0, 0);
|
||||
if (scale_x) *scale_x = (int)(r & 0xFFFFFFFF);
|
||||
if (scale_y) *scale_y = (int)(r >> 32);
|
||||
}
|
||||
|
||||
// Timekeeping (wall-clock)
|
||||
inline void gettime(Montauk::DateTime* out) { syscall1(Montauk::SYS_GETTIME, (uint64_t)out); }
|
||||
|
||||
// Random number generation
|
||||
inline int64_t getrandom(void* buf, uint32_t len) {
|
||||
return syscall2(Montauk::SYS_GETRANDOM, (uint64_t)buf, (uint64_t)len);
|
||||
}
|
||||
|
||||
// Power management
|
||||
[[noreturn]] inline void reset() {
|
||||
syscall0(Montauk::SYS_RESET);
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
[[noreturn]] inline void shutdown() {
|
||||
syscall0(Montauk::SYS_SHUTDOWN);
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
// Mouse
|
||||
inline void mouse_state(Montauk::MouseState* out) { syscall1(Montauk::SYS_MOUSESTATE, (uint64_t)out); }
|
||||
inline void set_mouse_bounds(int32_t maxX, int32_t maxY) {
|
||||
syscall2(Montauk::SYS_SETMOUSEBOUNDS, (uint64_t)maxX, (uint64_t)maxY);
|
||||
}
|
||||
|
||||
// Kernel log
|
||||
inline int64_t read_klog(char* buf, uint64_t size) {
|
||||
return syscall2(Montauk::SYS_KLOG, (uint64_t)buf, size);
|
||||
}
|
||||
|
||||
// I/O redirection
|
||||
inline int spawn_redir(const char* path, const char* args = nullptr) {
|
||||
return (int)syscall2(Montauk::SYS_SPAWN_REDIR, (uint64_t)path, (uint64_t)args);
|
||||
}
|
||||
inline int childio_read(int childPid, char* buf, int maxLen) {
|
||||
return (int)syscall3(Montauk::SYS_CHILDIO_READ, (uint64_t)childPid, (uint64_t)buf, (uint64_t)maxLen);
|
||||
}
|
||||
inline int childio_write(int childPid, const char* data, int len) {
|
||||
return (int)syscall3(Montauk::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
|
||||
}
|
||||
inline int childio_writekey(int childPid, const Montauk::KeyEvent* key) {
|
||||
return (int)syscall2(Montauk::SYS_CHILDIO_WRITEKEY, (uint64_t)childPid, (uint64_t)key);
|
||||
}
|
||||
inline int childio_settermsz(int childPid, int cols, int rows) {
|
||||
return (int)syscall3(Montauk::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
|
||||
}
|
||||
|
||||
// Process listing / kill
|
||||
inline int proclist(Montauk::ProcInfo* buf, int max) {
|
||||
return (int)syscall2(Montauk::SYS_PROCLIST, (uint64_t)buf, (uint64_t)max);
|
||||
}
|
||||
inline int kill(int pid) {
|
||||
return (int)syscall1(Montauk::SYS_KILL, (uint64_t)pid);
|
||||
}
|
||||
inline int devlist(Montauk::DevInfo* buf, int max) {
|
||||
return (int)syscall2(Montauk::SYS_DEVLIST, (uint64_t)buf, (uint64_t)max);
|
||||
}
|
||||
inline int diskinfo(Montauk::DiskInfo* buf, int port) {
|
||||
return (int)syscall2(Montauk::SYS_DISKINFO, (uint64_t)buf, (uint64_t)port);
|
||||
}
|
||||
|
||||
// Partition table
|
||||
inline int partlist(Montauk::PartInfo* buf, int max) {
|
||||
return (int)syscall2(Montauk::SYS_PARTLIST, (uint64_t)buf, (uint64_t)max);
|
||||
}
|
||||
|
||||
// Raw block device I/O (driver-agnostic)
|
||||
inline int64_t disk_read(int blockDev, uint64_t lba, uint32_t sectorCount, void* buf) {
|
||||
return syscall4(Montauk::SYS_DISKREAD, (uint64_t)blockDev, lba,
|
||||
(uint64_t)sectorCount, (uint64_t)buf);
|
||||
}
|
||||
inline int64_t disk_write(int blockDev, uint64_t lba, uint32_t sectorCount, const void* buf) {
|
||||
return syscall4(Montauk::SYS_DISKWRITE, (uint64_t)blockDev, lba,
|
||||
(uint64_t)sectorCount, (uint64_t)buf);
|
||||
}
|
||||
|
||||
// GPT management
|
||||
inline int gpt_init(int blockDev) {
|
||||
return (int)syscall1(Montauk::SYS_GPTINIT, (uint64_t)blockDev);
|
||||
}
|
||||
inline int gpt_add(const Montauk::GptAddParams* params) {
|
||||
return (int)syscall1(Montauk::SYS_GPTADD, (uint64_t)params);
|
||||
}
|
||||
inline int fs_mount(int partIndex, int driveNum) {
|
||||
return (int)syscall2(Montauk::SYS_FSMOUNT, (uint64_t)partIndex, (uint64_t)driveNum);
|
||||
}
|
||||
inline int fs_format(const Montauk::FsFormatParams* params) {
|
||||
return (int)syscall1(Montauk::SYS_FSFORMAT, (uint64_t)params);
|
||||
}
|
||||
|
||||
// Audio
|
||||
inline int audio_open(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
|
||||
return (int)syscall3(Montauk::SYS_AUDIOOPEN, (uint64_t)sampleRate,
|
||||
(uint64_t)channels, (uint64_t)bitsPerSample);
|
||||
}
|
||||
inline void audio_close(int handle) {
|
||||
syscall1(Montauk::SYS_AUDIOCLOSE, (uint64_t)handle);
|
||||
}
|
||||
inline int audio_write(int handle, const void* data, uint32_t size) {
|
||||
return (int)syscall3(Montauk::SYS_AUDIOWRITE, (uint64_t)handle,
|
||||
(uint64_t)data, (uint64_t)size);
|
||||
}
|
||||
inline int audio_ctl(int handle, int cmd, int value) {
|
||||
return (int)syscall3(Montauk::SYS_AUDIOCTL, (uint64_t)handle,
|
||||
(uint64_t)cmd, (uint64_t)value);
|
||||
}
|
||||
inline int audio_set_volume(int handle, int percent) {
|
||||
return audio_ctl(handle, Montauk::AUDIO_CTL_SET_VOLUME, percent);
|
||||
}
|
||||
inline int audio_get_volume(int handle) {
|
||||
return audio_ctl(handle, Montauk::AUDIO_CTL_GET_VOLUME, 0);
|
||||
}
|
||||
inline int audio_get_pos(int handle) {
|
||||
return audio_ctl(handle, Montauk::AUDIO_CTL_GET_POS, 0);
|
||||
}
|
||||
inline int audio_pause(int handle) {
|
||||
return audio_ctl(handle, Montauk::AUDIO_CTL_PAUSE, 1);
|
||||
}
|
||||
inline int audio_resume(int handle) {
|
||||
return audio_ctl(handle, Montauk::AUDIO_CTL_PAUSE, 0);
|
||||
}
|
||||
inline int audio_get_output(int handle) {
|
||||
return audio_ctl(handle, Montauk::AUDIO_CTL_GET_OUTPUT, 0);
|
||||
}
|
||||
inline int audio_bt_status(int handle) {
|
||||
return audio_ctl(handle, Montauk::AUDIO_CTL_BT_STATUS, 0);
|
||||
}
|
||||
|
||||
// Bluetooth
|
||||
inline int bt_scan(Montauk::BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
||||
return (int)syscall3(Montauk::SYS_BTSCAN, (uint64_t)buf, (uint64_t)maxCount, (uint64_t)timeoutMs);
|
||||
}
|
||||
inline int bt_connect(const uint8_t* bdAddr) {
|
||||
return (int)syscall1(Montauk::SYS_BTCONNECT, (uint64_t)bdAddr);
|
||||
}
|
||||
inline int bt_disconnect(const uint8_t* bdAddr) {
|
||||
return (int)syscall1(Montauk::SYS_BTDISCONNECT, (uint64_t)bdAddr);
|
||||
}
|
||||
inline int bt_list(Montauk::BtDevInfo* buf, int maxCount) {
|
||||
return (int)syscall2(Montauk::SYS_BTLIST, (uint64_t)buf, (uint64_t)maxCount);
|
||||
}
|
||||
inline int bt_info(Montauk::BtAdapterInfo* buf) {
|
||||
return (int)syscall1(Montauk::SYS_BTINFO, (uint64_t)buf);
|
||||
}
|
||||
|
||||
// Kernel introspection
|
||||
inline void memstats(Montauk::MemStats* out) { syscall1(Montauk::SYS_MEMSTATS, (uint64_t)out); }
|
||||
|
||||
// Window server
|
||||
inline int win_create(const char* title, int w, int h, Montauk::WinCreateResult* result) {
|
||||
return (int)syscall4(Montauk::SYS_WINCREATE, (uint64_t)title, (uint64_t)w, (uint64_t)h, (uint64_t)result);
|
||||
}
|
||||
inline int win_destroy(int id) {
|
||||
return (int)syscall1(Montauk::SYS_WINDESTROY, (uint64_t)id);
|
||||
}
|
||||
inline uint64_t win_present(int id) {
|
||||
return (uint64_t)syscall1(Montauk::SYS_WINPRESENT, (uint64_t)id);
|
||||
}
|
||||
inline int win_poll(int id, Montauk::WinEvent* event) {
|
||||
return (int)syscall2(Montauk::SYS_WINPOLL, (uint64_t)id, (uint64_t)event);
|
||||
}
|
||||
inline int win_enumerate(Montauk::WinInfo* info, int max) {
|
||||
return (int)syscall2(Montauk::SYS_WINENUM, (uint64_t)info, (uint64_t)max);
|
||||
}
|
||||
inline uint64_t win_map(int id) {
|
||||
return (uint64_t)syscall1(Montauk::SYS_WINMAP, (uint64_t)id);
|
||||
}
|
||||
inline int win_sendevent(int id, const Montauk::WinEvent* event) {
|
||||
return (int)syscall2(Montauk::SYS_WINSENDEVENT, (uint64_t)id, (uint64_t)event);
|
||||
}
|
||||
inline uint64_t win_resize(int id, int w, int h) {
|
||||
return (uint64_t)syscall3(Montauk::SYS_WINRESIZE, (uint64_t)id, (uint64_t)w, (uint64_t)h);
|
||||
}
|
||||
inline int win_setscale(int scale) {
|
||||
return (int)syscall1(Montauk::SYS_WINSETSCALE, (uint64_t)scale);
|
||||
}
|
||||
inline int win_getscale() {
|
||||
return (int)syscall0(Montauk::SYS_WINGETSCALE);
|
||||
}
|
||||
inline int win_setcursor(int id, int cursor) {
|
||||
return (int)syscall2(Montauk::SYS_WINSETCURSOR, (uint64_t)id, (uint64_t)cursor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
/*
|
||||
* toml.h
|
||||
* TOML config file parser
|
||||
* Supports: strings, integers, booleans, tables, arrays
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace montauk {
|
||||
namespace toml {
|
||||
|
||||
enum class Type { String, Int, Bool, Array, Table };
|
||||
|
||||
struct Value;
|
||||
|
||||
// Dynamic array of Value pointers (for arrays and table entries)
|
||||
struct ValueList {
|
||||
Value** items;
|
||||
int count;
|
||||
int cap;
|
||||
|
||||
void init() { items = nullptr; count = 0; cap = 0; }
|
||||
|
||||
void push(Value* v) {
|
||||
if (count >= cap) {
|
||||
int newCap = cap ? cap * 2 : 8;
|
||||
auto** buf = (Value**)montauk::malloc(newCap * sizeof(Value*));
|
||||
if (items) {
|
||||
montauk::memcpy(buf, items, count * sizeof(Value*));
|
||||
montauk::mfree(items);
|
||||
}
|
||||
items = buf;
|
||||
cap = newCap;
|
||||
}
|
||||
items[count++] = v;
|
||||
}
|
||||
|
||||
void destroy(); // forward — defined after Value
|
||||
};
|
||||
|
||||
struct Value {
|
||||
Type type;
|
||||
char* key; // owned, dotted path (e.g. "server.port"); null for array elements
|
||||
|
||||
union {
|
||||
char* str; // Type::String — owned
|
||||
int64_t ival; // Type::Int
|
||||
bool bval; // Type::Bool
|
||||
};
|
||||
ValueList array; // Type::Array elements or Type::Table entries
|
||||
|
||||
static Value* make_string(const char* k, const char* s, int slen) {
|
||||
auto* v = (Value*)montauk::malloc(sizeof(Value));
|
||||
v->type = Type::String;
|
||||
v->key = dup(k);
|
||||
v->str = dupn(s, slen);
|
||||
v->array.init();
|
||||
return v;
|
||||
}
|
||||
|
||||
static Value* make_int(const char* k, int64_t n) {
|
||||
auto* v = (Value*)montauk::malloc(sizeof(Value));
|
||||
v->type = Type::Int;
|
||||
v->key = dup(k);
|
||||
v->ival = n;
|
||||
v->array.init();
|
||||
return v;
|
||||
}
|
||||
|
||||
static Value* make_bool(const char* k, bool b) {
|
||||
auto* v = (Value*)montauk::malloc(sizeof(Value));
|
||||
v->type = Type::Bool;
|
||||
v->key = dup(k);
|
||||
v->bval = b;
|
||||
v->array.init();
|
||||
return v;
|
||||
}
|
||||
|
||||
static Value* make_array(const char* k) {
|
||||
auto* v = (Value*)montauk::malloc(sizeof(Value));
|
||||
v->type = Type::Array;
|
||||
v->key = dup(k);
|
||||
v->ival = 0;
|
||||
v->array.init();
|
||||
return v;
|
||||
}
|
||||
|
||||
static Value* make_table(const char* k) {
|
||||
auto* v = (Value*)montauk::malloc(sizeof(Value));
|
||||
v->type = Type::Table;
|
||||
v->key = dup(k);
|
||||
v->ival = 0;
|
||||
v->array.init();
|
||||
return v;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (key) montauk::mfree(key);
|
||||
if (type == Type::String && str) montauk::mfree(str);
|
||||
array.destroy();
|
||||
montauk::mfree(this);
|
||||
}
|
||||
|
||||
// helpers
|
||||
static char* dup(const char* s) {
|
||||
if (!s) return nullptr;
|
||||
int n = montauk::slen(s);
|
||||
return dupn(s, n);
|
||||
}
|
||||
static char* dupn(const char* s, int n) {
|
||||
char* d = (char*)montauk::malloc(n + 1);
|
||||
montauk::memcpy(d, s, n);
|
||||
d[n] = '\0';
|
||||
return d;
|
||||
}
|
||||
};
|
||||
|
||||
inline void ValueList::destroy() {
|
||||
for (int i = 0; i < count; i++) items[i]->destroy();
|
||||
if (items) montauk::mfree(items);
|
||||
items = nullptr;
|
||||
count = 0;
|
||||
cap = 0;
|
||||
}
|
||||
|
||||
// ---- Document ----
|
||||
|
||||
struct Doc {
|
||||
ValueList entries; // flat list of all top-level and nested values
|
||||
|
||||
void init() { entries.init(); }
|
||||
|
||||
void destroy() {
|
||||
entries.destroy();
|
||||
}
|
||||
|
||||
// Lookup by dotted key path (e.g. "server.port")
|
||||
Value* get(const char* key) const {
|
||||
for (int i = 0; i < entries.count; i++) {
|
||||
if (entries.items[i]->key && montauk::streq(entries.items[i]->key, key))
|
||||
return entries.items[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Typed accessors with defaults
|
||||
const char* get_string(const char* key, const char* def = "") const {
|
||||
auto* v = get(key);
|
||||
return (v && v->type == Type::String) ? v->str : def;
|
||||
}
|
||||
|
||||
int64_t get_int(const char* key, int64_t def = 0) const {
|
||||
auto* v = get(key);
|
||||
return (v && v->type == Type::Int) ? v->ival : def;
|
||||
}
|
||||
|
||||
bool get_bool(const char* key, bool def = false) const {
|
||||
auto* v = get(key);
|
||||
return (v && v->type == Type::Bool) ? v->bval : def;
|
||||
}
|
||||
|
||||
Value* get_array(const char* key) const {
|
||||
auto* v = get(key);
|
||||
return (v && v->type == Type::Array) ? v : nullptr;
|
||||
}
|
||||
|
||||
Value* get_table(const char* key) const {
|
||||
auto* v = get(key);
|
||||
return (v && v->type == Type::Table) ? v : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Parser ----
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct Parser {
|
||||
const char* src;
|
||||
int pos;
|
||||
int len;
|
||||
char table_prefix[256]; // current [table] prefix
|
||||
|
||||
char peek() const { return pos < len ? src[pos] : '\0'; }
|
||||
char next() { return pos < len ? src[pos++] : '\0'; }
|
||||
bool at_end() const { return pos >= len; }
|
||||
|
||||
void skip_ws() {
|
||||
while (pos < len && (src[pos] == ' ' || src[pos] == '\t')) pos++;
|
||||
}
|
||||
|
||||
void skip_line() {
|
||||
while (pos < len && src[pos] != '\n') pos++;
|
||||
if (pos < len) pos++; // consume newline
|
||||
}
|
||||
|
||||
void skip_ws_and_newlines() {
|
||||
while (pos < len) {
|
||||
char c = src[pos];
|
||||
if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
|
||||
pos++;
|
||||
else if (c == '#')
|
||||
skip_line();
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build dotted key: prefix.key
|
||||
void build_key(char* out, int outSz, const char* key, int keyLen) {
|
||||
int p = 0;
|
||||
if (table_prefix[0]) {
|
||||
int plen = montauk::slen(table_prefix);
|
||||
for (int i = 0; i < plen && p < outSz - 2; i++) out[p++] = table_prefix[i];
|
||||
out[p++] = '.';
|
||||
}
|
||||
for (int i = 0; i < keyLen && p < outSz - 1; i++) out[p++] = key[i];
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// Parse a bare key or quoted key, returns length
|
||||
int parse_key(char* buf, int bufSz) {
|
||||
skip_ws();
|
||||
int n = 0;
|
||||
if (peek() == '"') {
|
||||
next(); // consume opening quote
|
||||
while (!at_end() && peek() != '"' && n < bufSz - 1)
|
||||
buf[n++] = next();
|
||||
if (peek() == '"') next();
|
||||
} else {
|
||||
while (!at_end()) {
|
||||
char c = peek();
|
||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-') {
|
||||
buf[n++] = next();
|
||||
if (n >= bufSz - 1) break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
buf[n] = '\0';
|
||||
return n;
|
||||
}
|
||||
|
||||
// Parse a dotted key (e.g. server.host), returns full length written to buf
|
||||
int parse_dotted_key(char* buf, int bufSz) {
|
||||
int total = 0;
|
||||
// Parse first segment
|
||||
char seg[128];
|
||||
int segLen = parse_key(seg, sizeof(seg));
|
||||
for (int i = 0; i < segLen && total < bufSz - 1; i++)
|
||||
buf[total++] = seg[i];
|
||||
|
||||
// Parse additional dotted segments
|
||||
while (peek() == '.') {
|
||||
next(); // consume dot
|
||||
if (total < bufSz - 1) buf[total++] = '.';
|
||||
segLen = parse_key(seg, sizeof(seg));
|
||||
for (int i = 0; i < segLen && total < bufSz - 1; i++)
|
||||
buf[total++] = seg[i];
|
||||
}
|
||||
buf[total] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
int64_t parse_integer() {
|
||||
bool neg = false;
|
||||
if (peek() == '-') { neg = true; next(); }
|
||||
else if (peek() == '+') { next(); }
|
||||
|
||||
// Hex, octal, binary
|
||||
if (peek() == '0' && pos + 1 < len) {
|
||||
char c2 = src[pos + 1];
|
||||
if (c2 == 'x' || c2 == 'X') {
|
||||
pos += 2;
|
||||
int64_t val = 0;
|
||||
while (!at_end()) {
|
||||
char c = peek();
|
||||
if (c == '_') { next(); continue; }
|
||||
int d = -1;
|
||||
if (c >= '0' && c <= '9') d = c - '0';
|
||||
else if (c >= 'a' && c <= 'f') d = 10 + c - 'a';
|
||||
else if (c >= 'A' && c <= 'F') d = 10 + c - 'A';
|
||||
if (d < 0) break;
|
||||
val = val * 16 + d;
|
||||
next();
|
||||
}
|
||||
return neg ? -val : val;
|
||||
}
|
||||
if (c2 == 'o') {
|
||||
pos += 2;
|
||||
int64_t val = 0;
|
||||
while (!at_end()) {
|
||||
char c = peek();
|
||||
if (c == '_') { next(); continue; }
|
||||
if (c < '0' || c > '7') break;
|
||||
val = val * 8 + (c - '0');
|
||||
next();
|
||||
}
|
||||
return neg ? -val : val;
|
||||
}
|
||||
if (c2 == 'b') {
|
||||
pos += 2;
|
||||
int64_t val = 0;
|
||||
while (!at_end()) {
|
||||
char c = peek();
|
||||
if (c == '_') { next(); continue; }
|
||||
if (c != '0' && c != '1') break;
|
||||
val = val * 2 + (c - '0');
|
||||
next();
|
||||
}
|
||||
return neg ? -val : val;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t val = 0;
|
||||
while (!at_end()) {
|
||||
char c = peek();
|
||||
if (c == '_') { next(); continue; }
|
||||
if (c < '0' || c > '9') break;
|
||||
val = val * 10 + (c - '0');
|
||||
next();
|
||||
}
|
||||
return neg ? -val : val;
|
||||
}
|
||||
|
||||
// Parse string value (opening quote already detected but not consumed)
|
||||
// Returns heap-allocated string
|
||||
char* parse_string_value(int* outLen) {
|
||||
char quote = next(); // consume opening " or '
|
||||
bool literal = (quote == '\'');
|
||||
|
||||
// Multi-line? (""" or ''')
|
||||
bool multi = false;
|
||||
if (pos + 1 < len && src[pos] == quote && src[pos + 1] == quote) {
|
||||
pos += 2;
|
||||
multi = true;
|
||||
// Skip first newline after opening delimiter
|
||||
if (peek() == '\r') next();
|
||||
if (peek() == '\n') next();
|
||||
}
|
||||
|
||||
// Collect into temp buffer
|
||||
char buf[4096];
|
||||
int n = 0;
|
||||
|
||||
while (!at_end() && n < (int)sizeof(buf) - 1) {
|
||||
if (multi) {
|
||||
// Check for closing triple-quote
|
||||
if (pos + 2 < len && src[pos] == quote &&
|
||||
src[pos+1] == quote && src[pos+2] == quote) {
|
||||
pos += 3;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (peek() == quote) { next(); break; }
|
||||
if (peek() == '\n') break; // unterminated single-line
|
||||
}
|
||||
|
||||
if (!literal && peek() == '\\') {
|
||||
next(); // consume backslash
|
||||
char esc = next();
|
||||
switch (esc) {
|
||||
case 'n': buf[n++] = '\n'; break;
|
||||
case 't': buf[n++] = '\t'; break;
|
||||
case 'r': buf[n++] = '\r'; break;
|
||||
case '\\': buf[n++] = '\\'; break;
|
||||
case '"': buf[n++] = '"'; break;
|
||||
case '\r':
|
||||
if (peek() == '\n') next();
|
||||
skip_ws();
|
||||
break;
|
||||
case '\n':
|
||||
skip_ws();
|
||||
break;
|
||||
default: buf[n++] = esc; break;
|
||||
}
|
||||
} else {
|
||||
buf[n++] = next();
|
||||
}
|
||||
}
|
||||
|
||||
*outLen = n;
|
||||
return Value::dupn(buf, n);
|
||||
}
|
||||
|
||||
Value* parse_value(const char* fullKey) {
|
||||
skip_ws();
|
||||
char c = peek();
|
||||
|
||||
// String
|
||||
if (c == '"' || c == '\'') {
|
||||
int slen = 0;
|
||||
char* s = parse_string_value(&slen);
|
||||
auto* v = Value::make_string(fullKey, s, slen);
|
||||
montauk::mfree(s);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Boolean
|
||||
if (montauk::starts_with(src + pos, "true")) {
|
||||
pos += 4;
|
||||
return Value::make_bool(fullKey, true);
|
||||
}
|
||||
if (montauk::starts_with(src + pos, "false")) {
|
||||
pos += 5;
|
||||
return Value::make_bool(fullKey, false);
|
||||
}
|
||||
|
||||
// Array
|
||||
if (c == '[') {
|
||||
next(); // consume [
|
||||
auto* arr = Value::make_array(fullKey);
|
||||
skip_ws_and_newlines();
|
||||
while (!at_end() && peek() != ']') {
|
||||
auto* elem = parse_value(nullptr);
|
||||
if (elem) arr->array.push(elem);
|
||||
skip_ws_and_newlines();
|
||||
if (peek() == ',') next();
|
||||
skip_ws_and_newlines();
|
||||
}
|
||||
if (peek() == ']') next();
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Inline table
|
||||
if (c == '{') {
|
||||
next(); // consume {
|
||||
auto* tbl = Value::make_table(fullKey);
|
||||
skip_ws_and_newlines();
|
||||
while (!at_end() && peek() != '}') {
|
||||
char key[128];
|
||||
int keyLen = parse_dotted_key(key, sizeof(key));
|
||||
if (keyLen == 0) { skip_line(); continue; }
|
||||
skip_ws();
|
||||
if (peek() == '=') next();
|
||||
skip_ws();
|
||||
|
||||
// Build full dotted key for inline table entry
|
||||
char entryKey[256];
|
||||
int p = 0;
|
||||
if (fullKey) {
|
||||
int flen = montauk::slen(fullKey);
|
||||
for (int i = 0; i < flen && p < 254; i++) entryKey[p++] = fullKey[i];
|
||||
entryKey[p++] = '.';
|
||||
}
|
||||
for (int i = 0; i < keyLen && p < 255; i++) entryKey[p++] = key[i];
|
||||
entryKey[p] = '\0';
|
||||
|
||||
auto* val = parse_value(entryKey);
|
||||
if (val) tbl->array.push(val);
|
||||
skip_ws();
|
||||
if (peek() == ',') next();
|
||||
skip_ws();
|
||||
}
|
||||
if (peek() == '}') next();
|
||||
return tbl;
|
||||
}
|
||||
|
||||
// Integer (includes negative)
|
||||
if ((c >= '0' && c <= '9') || c == '+' || c == '-') {
|
||||
int64_t n = parse_integer();
|
||||
return Value::make_int(fullKey, n);
|
||||
}
|
||||
|
||||
// Unknown — skip the rest of the line
|
||||
skip_line();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void parse(Doc* doc) {
|
||||
table_prefix[0] = '\0';
|
||||
|
||||
while (!at_end()) {
|
||||
skip_ws_and_newlines();
|
||||
if (at_end()) break;
|
||||
|
||||
char c = peek();
|
||||
|
||||
// Table header: [name] or [name.sub]
|
||||
if (c == '[') {
|
||||
next();
|
||||
bool array_table = false;
|
||||
if (peek() == '[') { next(); array_table = true; }
|
||||
|
||||
skip_ws();
|
||||
int plen = parse_dotted_key(table_prefix, sizeof(table_prefix));
|
||||
skip_ws();
|
||||
if (peek() == ']') next();
|
||||
if (array_table && peek() == ']') next();
|
||||
|
||||
// Register the table itself
|
||||
auto* tbl = Value::make_table(table_prefix);
|
||||
doc->entries.push(tbl);
|
||||
|
||||
skip_line();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key = value
|
||||
char key[128];
|
||||
int keyLen = parse_dotted_key(key, sizeof(key));
|
||||
if (keyLen == 0) { skip_line(); continue; }
|
||||
|
||||
skip_ws();
|
||||
if (peek() != '=') { skip_line(); continue; }
|
||||
next(); // consume =
|
||||
skip_ws();
|
||||
|
||||
char fullKey[256];
|
||||
build_key(fullKey, sizeof(fullKey), key, keyLen);
|
||||
|
||||
Value* val = parse_value(fullKey);
|
||||
if (val) {
|
||||
doc->entries.push(val);
|
||||
|
||||
// For inline tables, also flatten entries into doc
|
||||
if (val->type == Type::Table) {
|
||||
for (int i = 0; i < val->array.count; i++) {
|
||||
// Clone entry into doc's flat list so dotted lookup works
|
||||
auto* e = val->array.items[i];
|
||||
Value* clone = nullptr;
|
||||
if (e->type == Type::String)
|
||||
clone = Value::make_string(e->key, e->str, montauk::slen(e->str));
|
||||
else if (e->type == Type::Int)
|
||||
clone = Value::make_int(e->key, e->ival);
|
||||
else if (e->type == Type::Bool)
|
||||
clone = Value::make_bool(e->key, e->bval);
|
||||
if (clone) doc->entries.push(clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Consume rest of line (trailing comment, etc.)
|
||||
skip_ws();
|
||||
if (peek() == '#') skip_line();
|
||||
else if (peek() == '\r' || peek() == '\n') {
|
||||
if (peek() == '\r') next();
|
||||
if (peek() == '\n') next();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ---- Public API ----
|
||||
|
||||
// Parse a TOML string into a Doc. Caller must call doc.destroy() when done.
|
||||
inline Doc parse(const char* text) {
|
||||
Doc doc;
|
||||
doc.init();
|
||||
|
||||
detail::Parser p;
|
||||
p.src = text;
|
||||
p.pos = 0;
|
||||
p.len = montauk::slen(text);
|
||||
p.table_prefix[0] = '\0';
|
||||
p.parse(&doc);
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
// Parse from a file descriptor (reads entire file into buffer first).
|
||||
// Requires montauk::syscall read/fstat or similar — omitted here for portability.
|
||||
// Users can read a file into a buffer and call parse() directly.
|
||||
|
||||
} // namespace toml
|
||||
} // namespace montauk
|
||||
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* user.h
|
||||
* User database, password hashing, and authentication for MontaukOS
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/config.h>
|
||||
#include <bearssl_hash.h>
|
||||
|
||||
namespace montauk {
|
||||
namespace user {
|
||||
|
||||
static constexpr int MAX_USERS = 16;
|
||||
|
||||
struct UserInfo {
|
||||
char username[32];
|
||||
char display_name[64];
|
||||
char password_hash[65]; // 64 hex chars + null
|
||||
char salt[33]; // 32 hex chars + null
|
||||
char role[16];
|
||||
};
|
||||
|
||||
// ---- String helpers ----
|
||||
|
||||
inline void str_cat(char* dst, const char* src, int max) {
|
||||
int len = montauk::slen(dst);
|
||||
int i = 0;
|
||||
while (src[i] && len < max - 1) {
|
||||
dst[len++] = src[i++];
|
||||
}
|
||||
dst[len] = '\0';
|
||||
}
|
||||
|
||||
inline void build_user_key(char* out, int sz, const char* username, const char* field) {
|
||||
int p = 0;
|
||||
const char* prefix = "users.";
|
||||
while (*prefix && p < sz - 1) out[p++] = *prefix++;
|
||||
while (*username && p < sz - 1) out[p++] = *username++;
|
||||
if (p < sz - 1) out[p++] = '.';
|
||||
while (*field && p < sz - 1) out[p++] = *field++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// ---- Path helpers ----
|
||||
|
||||
inline void home_dir(const char* username, char* out, int sz) {
|
||||
int p = 0;
|
||||
const char* prefix = "0:/users/";
|
||||
while (*prefix && p < sz - 1) out[p++] = *prefix++;
|
||||
while (*username && p < sz - 1) out[p++] = *username++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
inline void config_dir(const char* username, char* out, int sz) {
|
||||
home_dir(username, out, sz);
|
||||
str_cat(out, "/config", sz);
|
||||
}
|
||||
|
||||
// ---- Hex conversion helpers ----
|
||||
|
||||
inline void bytes_to_hex(const uint8_t* bytes, int len, char* out) {
|
||||
const char* digits = "0123456789abcdef";
|
||||
for (int i = 0; i < len; i++) {
|
||||
out[i * 2] = digits[(bytes[i] >> 4) & 0x0F];
|
||||
out[i * 2 + 1] = digits[bytes[i] & 0x0F];
|
||||
}
|
||||
out[len * 2] = '\0';
|
||||
}
|
||||
|
||||
inline int hex_char_val(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
|
||||
if (c >= 'A' && c <= 'F') return 10 + c - 'A';
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline int hex_to_bytes(const char* hex, uint8_t* out, int maxBytes) {
|
||||
int i = 0;
|
||||
while (hex[i * 2] && hex[i * 2 + 1] && i < maxBytes) {
|
||||
int hi = hex_char_val(hex[i * 2]);
|
||||
int lo = hex_char_val(hex[i * 2 + 1]);
|
||||
if (hi < 0 || lo < 0) return i;
|
||||
out[i] = (uint8_t)((hi << 4) | lo);
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
// ---- Password hashing (SHA-256) ----
|
||||
|
||||
inline void hash_password(const char* password, const char* salt_hex, char* out_hex64) {
|
||||
uint8_t salt_bytes[16];
|
||||
int salt_len = hex_to_bytes(salt_hex, salt_bytes, 16);
|
||||
int pass_len = montauk::slen(password);
|
||||
|
||||
br_sha256_context ctx;
|
||||
br_sha256_init(&ctx);
|
||||
br_sha256_update(&ctx, salt_bytes, salt_len);
|
||||
br_sha256_update(&ctx, password, pass_len);
|
||||
|
||||
uint8_t hash[32];
|
||||
br_sha256_out(&ctx, hash);
|
||||
bytes_to_hex(hash, 32, out_hex64);
|
||||
}
|
||||
|
||||
inline bool verify_password(const char* password, const char* salt_hex, const char* stored_hex64) {
|
||||
char computed[65];
|
||||
hash_password(password, salt_hex, computed);
|
||||
|
||||
int diff = 0;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
diff |= computed[i] ^ stored_hex64[i];
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
// ---- User database I/O ----
|
||||
|
||||
inline int load_users(UserInfo* buf, int max) {
|
||||
auto doc = config::load("users");
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < doc.entries.count && count < max; i++) {
|
||||
auto* v = doc.entries.items[i];
|
||||
if (!v->key) continue;
|
||||
|
||||
if (!montauk::starts_with(v->key, "users.")) continue;
|
||||
const char* after = v->key + 6;
|
||||
|
||||
int dot = -1;
|
||||
for (int j = 0; after[j]; j++) {
|
||||
if (after[j] == '.') { dot = j; break; }
|
||||
}
|
||||
if (dot <= 0) continue;
|
||||
|
||||
if (!montauk::streq(after + dot + 1, "display_name")) continue;
|
||||
|
||||
UserInfo* u = &buf[count];
|
||||
montauk::memset(u, 0, sizeof(UserInfo));
|
||||
int ulen = dot < 31 ? dot : 31;
|
||||
montauk::memcpy(u->username, after, ulen);
|
||||
u->username[ulen] = '\0';
|
||||
|
||||
if (v->type == montauk::toml::Type::String && v->str)
|
||||
montauk::strncpy(u->display_name, v->str, sizeof(u->display_name));
|
||||
|
||||
char prefix[64];
|
||||
int plen = 0;
|
||||
const char* pfx = "users.";
|
||||
while (*pfx) prefix[plen++] = *pfx++;
|
||||
for (int j = 0; j < ulen; j++) prefix[plen++] = u->username[j];
|
||||
prefix[plen] = '\0';
|
||||
|
||||
char key[96];
|
||||
|
||||
montauk::strcpy(key, prefix);
|
||||
str_cat(key, ".password_hash", 96);
|
||||
const char* ph = doc.get_string(key, "");
|
||||
montauk::strncpy(u->password_hash, ph, sizeof(u->password_hash));
|
||||
|
||||
montauk::strcpy(key, prefix);
|
||||
str_cat(key, ".salt", 96);
|
||||
const char* s = doc.get_string(key, "");
|
||||
montauk::strncpy(u->salt, s, sizeof(u->salt));
|
||||
|
||||
montauk::strcpy(key, prefix);
|
||||
str_cat(key, ".role", 96);
|
||||
const char* r = doc.get_string(key, "user");
|
||||
montauk::strncpy(u->role, r, sizeof(u->role));
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
doc.destroy();
|
||||
return count;
|
||||
}
|
||||
|
||||
inline int save_users(const UserInfo* buf, int count) {
|
||||
montauk::toml::Doc doc;
|
||||
doc.init();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const UserInfo* u = &buf[i];
|
||||
char key[96];
|
||||
|
||||
build_user_key(key, 96, u->username, "display_name");
|
||||
config::set_string(&doc, key, u->display_name);
|
||||
|
||||
build_user_key(key, 96, u->username, "password_hash");
|
||||
config::set_string(&doc, key, u->password_hash);
|
||||
|
||||
build_user_key(key, 96, u->username, "salt");
|
||||
config::set_string(&doc, key, u->salt);
|
||||
|
||||
build_user_key(key, 96, u->username, "role");
|
||||
config::set_string(&doc, key, u->role);
|
||||
}
|
||||
|
||||
int ret = config::save("users", &doc);
|
||||
doc.destroy();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ---- Authentication ----
|
||||
|
||||
inline bool authenticate(const char* username, const char* password) {
|
||||
UserInfo users[MAX_USERS];
|
||||
int count = load_users(users, MAX_USERS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (montauk::streq(users[i].username, username)) {
|
||||
return verify_password(password, users[i].salt, users[i].password_hash);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- User management ----
|
||||
|
||||
inline bool create_user(const char* username, const char* display_name,
|
||||
const char* password, const char* role) {
|
||||
UserInfo users[MAX_USERS];
|
||||
int count = load_users(users, MAX_USERS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (montauk::streq(users[i].username, username)) return false;
|
||||
}
|
||||
if (count >= MAX_USERS) return false;
|
||||
|
||||
UserInfo* u = &users[count];
|
||||
montauk::memset(u, 0, sizeof(UserInfo));
|
||||
montauk::strncpy(u->username, username, 31);
|
||||
montauk::strncpy(u->display_name, display_name, 63);
|
||||
montauk::strncpy(u->role, role, 15);
|
||||
|
||||
uint8_t salt_bytes[16];
|
||||
montauk::getrandom(salt_bytes, 16);
|
||||
bytes_to_hex(salt_bytes, 16, u->salt);
|
||||
|
||||
hash_password(password, u->salt, u->password_hash);
|
||||
|
||||
count++;
|
||||
save_users(users, count);
|
||||
|
||||
// Create home directory structure
|
||||
char dir[128];
|
||||
home_dir(username, dir, sizeof(dir));
|
||||
montauk::fmkdir(dir);
|
||||
|
||||
char subdir[128];
|
||||
montauk::strcpy(subdir, dir);
|
||||
str_cat(subdir, "/config", 128);
|
||||
montauk::fmkdir(subdir);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool delete_user(const char* username) {
|
||||
UserInfo users[MAX_USERS];
|
||||
int count = load_users(users, MAX_USERS);
|
||||
|
||||
int idx = -1;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (montauk::streq(users[i].username, username)) { idx = i; break; }
|
||||
}
|
||||
if (idx < 0) return false;
|
||||
|
||||
for (int i = idx; i < count - 1; i++) {
|
||||
users[i] = users[i + 1];
|
||||
}
|
||||
count--;
|
||||
|
||||
save_users(users, count);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool change_password(const char* username, const char* new_password) {
|
||||
UserInfo users[MAX_USERS];
|
||||
int count = load_users(users, MAX_USERS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (montauk::streq(users[i].username, username)) {
|
||||
uint8_t salt_bytes[16];
|
||||
montauk::getrandom(salt_bytes, 16);
|
||||
bytes_to_hex(salt_bytes, 16, users[i].salt);
|
||||
|
||||
hash_password(new_password, users[i].salt, users[i].password_hash);
|
||||
|
||||
save_users(users, count);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Session (current logged-in user) ----
|
||||
|
||||
// Write the current session after successful login.
|
||||
// Stores username in 0:/config/session.toml so any app can query it.
|
||||
inline void set_session(const char* username) {
|
||||
montauk::toml::Doc doc;
|
||||
doc.init();
|
||||
config::set_string(&doc, "session.username", username);
|
||||
config::save("session", &doc);
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
// Clear the session (on logout).
|
||||
inline void clear_session() {
|
||||
montauk::fdelete("0:/config/session.toml");
|
||||
}
|
||||
|
||||
// Read the current username into buf. Returns true if a session exists.
|
||||
inline bool get_session_username(char* buf, int bufSz) {
|
||||
auto doc = config::load("session");
|
||||
const char* name = doc.get_string("session.username", "");
|
||||
bool found = (name[0] != '\0');
|
||||
if (found) {
|
||||
montauk::strncpy(buf, name, bufSz);
|
||||
}
|
||||
doc.destroy();
|
||||
return found;
|
||||
}
|
||||
|
||||
// Get the current user's home directory. Returns true if a session exists.
|
||||
inline bool get_home_dir(char* buf, int bufSz) {
|
||||
char username[32];
|
||||
if (!get_session_username(username, sizeof(username))) return false;
|
||||
home_dir(username, buf, bufSz);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace user
|
||||
} // namespace montauk
|
||||
Reference in New Issue
Block a user