diff --git a/apps/boko-framework/README.md b/apps/boko-framework/README.md index d5f7856..ba82c99 100644 --- a/apps/boko-framework/README.md +++ b/apps/boko-framework/README.md @@ -4,12 +4,14 @@ Boko is a lightweight, multi-threaded web framework that brings FastAPI-style routing to the Bux programming language. It handles HTTP parsing, path pattern matching, query parameter extraction, and response building so you can focus on your application logic. +This version (0.2.0) is rewritten with modern Bux: `struct` methods, generic `StringMap` for headers/query/path params, `for` loops, algebraic enums, `StringBuilder`, and string interpolation. + ## Quick Start ```bash cd apps/boko-framework ../../buxc build -./boko-framework +./build/boko-framework ``` Open `http://localhost:8080` — you'll see the demo landing page. @@ -20,7 +22,7 @@ Boko follows a **single-dispatch-function** pattern. You define one function — ```bux import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound, - Path_Match, Request_GetQuery, Request_GetPathParam, + Path_Match, App, App_New, App_Run}; func Boko_Router(req: Request) -> Response { @@ -36,14 +38,14 @@ func Boko_Router(req: Request) -> Response { // Route: GET /users/{id} — path parameter if Path_Match("/users/{id}", req.path, &req) { - let id: String = Request_GetPathParam(&req, "id"); + let id: String = req.GetPathParam("id"); // Build JSON response with id return Response_Json(...); } // Route: GET /search?q=... — query parameter if String_Eq(req.path, "/search") { - let q: String = Request_GetQuery(&req, "q"); + let q: String = req.GetQuery("q"); return Response_Json(...); } @@ -63,16 +65,19 @@ func Main() -> int { | Field | Type | Description | |-------|------|-------------| -| `method` | `HttpVerb` | GET, POST, PUT, DELETE | +| `method` | `HttpVerb` | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS | | `path` | `String` | Request path (without query string) | | `body` | `String` | Request body (for POST/PUT) | -| `headerCount` | `int` | Number of headers | +| `headers` | `StringMap` | Parsed request headers | +| `query` | `StringMap` | Parsed query parameters | +| `pathParams` | `StringMap` | Extracted path parameters | -| Function | Returns | Description | -|----------|---------|-------------| -| `Request_GetHeader(req, name)` | `String` | Get header value by name | -| `Request_GetQuery(req, name)` | `String` | Get query parameter by name | -| `Request_GetPathParam(req, name)` | `String` | Get extracted path parameter | +| Method | Returns | Description | +|--------|---------|-------------| +| `req.GetHeader(name)` | `String` | Get header value by name | +| `req.GetQuery(name)` | `String` | Get query parameter by name | +| `req.HasQuery(name)` | `bool` | Check if query parameter exists | +| `req.GetPathParam(name)` | `String` | Get extracted path parameter | ### Response @@ -96,12 +101,12 @@ func Main() -> int { // Extracts: id=42, postId=7 if Path_Match("/users/{id}/posts/{postId}", req.path, &req) { - let userId: String = Request_GetPathParam(&req, "id"); // "42" - let postId: String = Request_GetPathParam(&req, "postId"); // "7" + let userId: String = req.GetPathParam("id"); // "42" + let postId: String = req.GetPathParam("postId"); // "7" } ``` -The function returns `true` if the pattern matches and populates `req.pathParamKeys` / `req.pathParamValues`. +The function returns `true` if the pattern matches and populates `req.pathParams`. ### App diff --git a/apps/boko-framework/bux.toml b/apps/boko-framework/bux.toml index 57209c1..9d7819f 100644 --- a/apps/boko-framework/bux.toml +++ b/apps/boko-framework/bux.toml @@ -1,6 +1,6 @@ [Package] Name = "boko-framework" -Version = "0.1.0" +Version = "0.2.0" Type = "bin" Description = "Boko — async web framework for Bux, inspired by FastAPI" diff --git a/apps/boko-framework/src/Boko.bux b/apps/boko-framework/src/Boko.bux index 15c0e5b..77371cb 100644 --- a/apps/boko-framework/src/Boko.bux +++ b/apps/boko-framework/src/Boko.bux @@ -1,28 +1,22 @@ // ============================================================================= // Boko — Async Web Framework for Bux (inspired by FastAPI) +// Rewritten with modern Bux: methods on structs, generic StringMap, for-in +// loops, algebraic enums, StringBuilder, and string interpolation. // ============================================================================= module Boko { import Std::Io::{PrintLine, Print, PrintInt}; import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError}; -import Std::String::{String_Len, String_Eq, String_StartsWith, String_Contains, String_Offset}; -import Std::Task::{Task_Join, TaskHandle}; +import Std::String::{ + String_Len, String_Eq, String_IsNull, + String_StartsWith, String_Contains, + String_Find, String_Offset, String_Slice, + String_SplitCount, String_SplitPart, + StringBuilder, StringBuilder_New, StringBuilder_Append, + StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free +}; +import Std::Map::{StringMap, StringMap_New, StringMap_Set, StringMap_Get, StringMap_Has}; -extern func bux_alloc(size: uint) -> *void; -extern func bux_strlen(s: String) -> uint; -extern func bux_strcmp(a: String, b: String) -> int; -extern func bux_strstr(haystack: String, needle: String) -> String; -extern func bux_str_slice(s: String, start: uint, len: uint) -> String; -extern func bux_str_split_count(s: String, delim: String) -> uint; -extern func bux_str_split_part(s: String, delim: String, index: uint) -> String; -extern func bux_sb_new(initial_cap: uint) -> *void; -extern func bux_sb_append(sb: *void, s: String); -extern func bux_sb_append_int(sb: *void, n: int64); -extern func bux_sb_append_char(sb: *void, c: char8); -extern func bux_sb_build(sb: *void) -> String; -extern func bux_sb_free(sb: *void); -extern func bux_base64url_encode(data: String, len: int) -> String; -extern func bux_base64url_decode(data: String, len: int, outlen: *int) -> String; // ============================================================================= // HTTP Methods @@ -37,6 +31,28 @@ enum HttpVerb { OPTIONS, } +func HttpVerb_MethodName(verb: HttpVerb) -> String { + if verb.tag == HttpVerb_GET { return "GET"; } + if verb.tag == HttpVerb_POST { return "POST"; } + if verb.tag == HttpVerb_PUT { return "PUT"; } + if verb.tag == HttpVerb_DELETE { return "DELETE"; } + if verb.tag == HttpVerb_PATCH { return "PATCH"; } + if verb.tag == HttpVerb_HEAD { return "HEAD"; } + if verb.tag == HttpVerb_OPTIONS { return "OPTIONS"; } + return "?"; +} + +func HttpVerb_Parse(methodStr: String) -> HttpVerb { + if String_Eq(methodStr, "GET") { return HttpVerb { tag: HttpVerb_GET }; } + if String_Eq(methodStr, "POST") { return HttpVerb { tag: HttpVerb_POST }; } + if String_Eq(methodStr, "PUT") { return HttpVerb { tag: HttpVerb_PUT }; } + if String_Eq(methodStr, "DELETE") { return HttpVerb { tag: HttpVerb_DELETE }; } + if String_Eq(methodStr, "PATCH") { return HttpVerb { tag: HttpVerb_PATCH }; } + if String_Eq(methodStr, "HEAD") { return HttpVerb { tag: HttpVerb_HEAD }; } + if String_Eq(methodStr, "OPTIONS") { return HttpVerb { tag: HttpVerb_OPTIONS }; } + return HttpVerb { tag: HttpVerb_GET }; +} + // ============================================================================= // Request — parsed incoming HTTP request // ============================================================================= @@ -44,15 +60,36 @@ struct Request { method: HttpVerb, path: String, body: String, - headerKeys: *String, - headerValues: *String, - headerCount: int, - queryKeys: *String, - queryValues: *String, - queryCount: int, - pathParamKeys: *String, - pathParamValues: *String, - pathParamCount: int, + headers: StringMap, + query: StringMap, + pathParams: StringMap, +} + +extend Request { + func GetHeader(self: Request, name: String) -> String { + if StringMap_Has(&self.headers, name) { + return StringMap_Get(&self.headers, name); + } + return ""; + } + + func GetQuery(self: Request, name: String) -> String { + if StringMap_Has(&self.query, name) { + return StringMap_Get(&self.query, name); + } + return ""; + } + + func HasQuery(self: Request, name: String) -> bool { + return StringMap_Has(&self.query, name); + } + + func GetPathParam(self: Request, name: String) -> String { + if StringMap_Has(&self.pathParams, name) { + return StringMap_Get(&self.pathParams, name); + } + return ""; + } } // ============================================================================= @@ -65,63 +102,9 @@ struct Response { extraHeaders: String, } -// ============================================================================= -// App — server instance -// ============================================================================= -struct App { - port: int, - threadCount: int, - serverName: String, -} - -// ============================================================================= -// Request helpers -// ============================================================================= - -func Request_GetHeader(req: *Request, name: String) -> String { - var i: int = 0; - while i < req.headerCount { - if bux_strcmp(req.headerKeys[i], name) == 0 { - return req.headerValues[i]; - } - i = i + 1; - } - return ""; -} - -func Request_GetQuery(req: *Request, name: String) -> String { - var i: int = 0; - while i < req.queryCount { - if bux_strcmp(req.queryKeys[i], name) == 0 { - return req.queryValues[i]; - } - i = i + 1; - } - return ""; -} - -func Request_GetPathParam(req: *Request, name: String) -> String { - var i: int = 0; - while i < req.pathParamCount { - if bux_strcmp(req.pathParamKeys[i], name) == 0 { - return req.pathParamValues[i]; - } - i = i + 1; - } - return ""; -} - -// ============================================================================= -// Response constructors -// ============================================================================= - +// --- Constructors --- func Response_New(status: int, contentType: String, body: String) -> Response { - var resp: Response; - resp.statusCode = status; - resp.contentType = contentType; - resp.body = body; - resp.extraHeaders = ""; - return resp; + return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" }; } func Response_Ok(body: String) -> Response { @@ -141,118 +124,143 @@ func Response_Text(text: String) -> Response { } func Response_Redirect(url: String) -> Response { - var resp: Response; - resp.statusCode = 302; - resp.contentType = ""; - resp.body = ""; - let sb: *void = bux_sb_new(256); - bux_sb_append(sb, "Location: "); - bux_sb_append(sb, url); - bux_sb_append(sb, "\r\n"); - resp.extraHeaders = bux_sb_build(sb); - bux_sb_free(sb); - return resp; + let sb: StringBuilder = StringBuilder_New(); + StringBuilder_Append(&sb, "Location: "); + StringBuilder_Append(&sb, url); + StringBuilder_Append(&sb, "\r\n"); + let headers: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); + return Response { statusCode: 302, contentType: "", body: "", extraHeaders: headers }; } func Response_NotFound() -> Response { - return Response_New(404, "application/json; charset=utf-8", - "{\"error\":\"not_found\"}"); + return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}"); } func Response_Error(status: int, message: String) -> Response { - return Response_New(status, "application/json; charset=utf-8", message); + let sb: StringBuilder = StringBuilder_New(); + StringBuilder_Append(&sb, "{\"error\":\""); + StringBuilder_Append(&sb, message); + StringBuilder_Append(&sb, "\"}"); + let body: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); + return Response_New(status, "application/json; charset=utf-8", body); } func Response_NoContent() -> Response { return Response_New(204, "", ""); } +// --- Instance helpers --- +extend Response { + func WithHeader(self: Response, key: String, value: String) -> Response { + let sb: StringBuilder = StringBuilder_New(); + StringBuilder_Append(&sb, self.extraHeaders); + StringBuilder_Append(&sb, key); + StringBuilder_Append(&sb, ": "); + StringBuilder_Append(&sb, value); + StringBuilder_Append(&sb, "\r\n"); + self.extraHeaders = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); + return self; + } +} + +// ============================================================================= +// Status phrase lookup +// ============================================================================= +func StatusPhrase(code: int) -> String { + if code == 200 { return "OK"; } + if code == 201 { return "Created"; } + if code == 204 { return "No Content"; } + if code == 301 { return "Moved Permanently"; } + if code == 302 { return "Found"; } + if code == 400 { return "Bad Request"; } + if code == 401 { return "Unauthorized"; } + if code == 403 { return "Forbidden"; } + if code == 404 { return "Not Found"; } + if code == 500 { return "Internal Server Error"; } + if code == 502 { return "Bad Gateway"; } + if code == 503 { return "Service Unavailable"; } + return "OK"; +} + // ============================================================================= // Build HTTP response string from Response struct // ============================================================================= func Response_Build(resp: Response) -> String { - let sb: *void = bux_sb_new(2048); + let sb: StringBuilder = StringBuilder_New(); // Status line - bux_sb_append(sb, "HTTP/1.1 "); - bux_sb_append_int(sb, resp.statusCode as int64); - bux_sb_append(sb, " "); - if resp.statusCode == 200 { bux_sb_append(sb, "OK"); } - else if resp.statusCode == 201 { bux_sb_append(sb, "Created"); } - else if resp.statusCode == 204 { bux_sb_append(sb, "No Content"); } - else if resp.statusCode == 302 { bux_sb_append(sb, "Found"); } - else if resp.statusCode == 400 { bux_sb_append(sb, "Bad Request"); } - else if resp.statusCode == 404 { bux_sb_append(sb, "Not Found"); } - else if resp.statusCode == 500 { bux_sb_append(sb, "Internal Server Error"); } - else { bux_sb_append(sb, "OK"); } - bux_sb_append(sb, "\r\n"); + StringBuilder_Append(&sb, "HTTP/1.1 "); + StringBuilder_AppendInt(&sb, resp.statusCode as int64); + StringBuilder_Append(&sb, " "); + StringBuilder_Append(&sb, StatusPhrase(resp.statusCode)); + StringBuilder_Append(&sb, "\r\n"); // Server - bux_sb_append(sb, "Server: Boko/0.1.0 (Bux)\r\n"); + StringBuilder_Append(&sb, "Server: Boko/0.2.0 (Bux)\r\n"); - // Extra headers (redirects, CORS, etc.) - if bux_strlen(resp.extraHeaders) > 0 { - bux_sb_append(sb, resp.extraHeaders); + // Extra headers + if String_Len(resp.extraHeaders) > 0 { + StringBuilder_Append(&sb, resp.extraHeaders); } // Content-Type - if bux_strlen(resp.contentType) > 0 { - bux_sb_append(sb, "Content-Type: "); - bux_sb_append(sb, resp.contentType); - bux_sb_append(sb, "\r\n"); + if String_Len(resp.contentType) > 0 { + StringBuilder_Append(&sb, "Content-Type: "); + StringBuilder_Append(&sb, resp.contentType); + StringBuilder_Append(&sb, "\r\n"); } // Content-Length - let bodyLen: uint = bux_strlen(resp.body); - bux_sb_append(sb, "Content-Length: "); - bux_sb_append_int(sb, bodyLen as int64); - bux_sb_append(sb, "\r\n"); + let bodyLen: uint = String_Len(resp.body); + StringBuilder_Append(&sb, "Content-Length: "); + StringBuilder_AppendInt(&sb, bodyLen as int64); + StringBuilder_Append(&sb, "\r\n"); // Connection close - bux_sb_append(sb, "Connection: close\r\n"); - - bux_sb_append(sb, "\r\n"); + StringBuilder_Append(&sb, "Connection: close\r\n"); + StringBuilder_Append(&sb, "\r\n"); if bodyLen > 0 { - bux_sb_append(sb, resp.body); + StringBuilder_Append(&sb, resp.body); } - let result: String = bux_sb_build(sb); - bux_sb_free(sb); + let result: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); return result; } // ============================================================================= -// Parse query string: ?key=val&key2=val2 → key-value arrays +// Parse query string: ?key=val&key2=val2 → generic StringMap // ============================================================================= -func Query_Parse(req: *Request, queryString: String) { - if bux_strlen(queryString) == 0 { return; } - let pairCount: uint = bux_str_split_count(queryString, "&"); - let cap: int = pairCount as int + 1; - req.queryKeys = bux_alloc(cap as uint * 8) as *String; - req.queryValues = bux_alloc(cap as uint * 8) as *String; - req.queryCount = 0; +func Query_Parse(queryString: String) -> StringMap { + let map: StringMap = StringMap_New(8); + if String_Len(queryString) == 0 { + return map; + } + let pairCount: uint = String_SplitCount(queryString, "&"); var i: uint = 0; while i < pairCount { - let pair: String = bux_str_split_part(queryString, "&", i); - let eqPos: String = bux_strstr(pair, "="); + let pair: String = String_SplitPart(queryString, "&", i); + let eqPos: String = String_Find(pair, "="); var key: String = pair; var value: String = ""; - if String_Len(eqPos) > 0 { + if !String_IsNull(eqPos) { let keyLen: uint = String_Offset(eqPos, pair); - key = bux_str_slice(pair, 0, keyLen); + key = String_Slice(pair, 0, keyLen); let valStart: uint = keyLen + 1; - let pairLen: uint = bux_strlen(pair); + let pairLen: uint = String_Len(pair); if valStart < pairLen { - value = bux_str_slice(pair, valStart, pairLen - valStart); + value = String_Slice(pair, valStart, pairLen - valStart); } } - req.queryKeys[req.queryCount] = key; - req.queryValues[req.queryCount] = value; - req.queryCount = req.queryCount + 1; + StringMap_Set(&map, key, value); i = i + 1; } + return map; } // ============================================================================= @@ -263,50 +271,45 @@ func Request_Parse(raw: String) -> Request { req.method = HttpVerb { tag: HttpVerb_GET }; req.path = "/"; req.body = ""; - req.headerCount = 0; - req.queryCount = 0; - req.pathParamCount = 0; + req.headers = StringMap_New(8); + req.query = StringMap_New(4); + req.pathParams = StringMap_New(4); if String_Len(raw) == 0 { return req; } - let rawLen: uint = bux_strlen(raw); - if rawLen == 0 { return req; } // Find header/body boundary - let boundary: String = bux_strstr(raw, "\r\n\r\n"); + let boundary: String = String_Find(raw, "\r\n\r\n"); + let rawLen: uint = String_Len(raw); var headerLen: uint = rawLen; - if String_Len(boundary) > 0 { + if !String_IsNull(boundary) { headerLen = String_Offset(boundary, raw); let bodyStart: uint = headerLen + 4; if bodyStart < rawLen { - req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart); + req.body = String_Slice(raw, bodyStart, rawLen - bodyStart); } } if headerLen == 0 { return req; } - let headerBlock: String = bux_str_slice(raw, 0, headerLen); - let lineCount: uint = bux_str_split_count(headerBlock, "\r\n"); + let headerBlock: String = String_Slice(raw, 0, headerLen); + let lineCount: uint = String_SplitCount(headerBlock, "\r\n"); if lineCount == 0 { return req; } // Parse request line - let requestLine: String = bux_str_split_part(headerBlock, "\r\n", 0); - if bux_strlen(requestLine) > 0 { - let methodStr: String = bux_str_split_part(requestLine, " ", 0); - if String_Eq(methodStr, "GET") { req.method = HttpVerb { tag: HttpVerb_GET }; } - else if String_Eq(methodStr, "POST") { req.method = HttpVerb { tag: HttpVerb_POST }; } - else if String_Eq(methodStr, "PUT") { req.method = HttpVerb { tag: HttpVerb_PUT }; } - else if String_Eq(methodStr, "DELETE") { req.method = HttpVerb { tag: HttpVerb_DELETE }; } + let requestLine: String = String_SplitPart(headerBlock, "\r\n", 0); + if String_Len(requestLine) > 0 { + let methodStr: String = String_SplitPart(requestLine, " ", 0); + req.method = HttpVerb_Parse(methodStr); - let fullPath: String = bux_str_split_part(requestLine, " ", 1); - // Split path and query string - let qmark: String = bux_strstr(fullPath, "?"); - if String_Len(qmark) > 0 { + let fullPath: String = String_SplitPart(requestLine, " ", 1); + let qmark: String = String_Find(fullPath, "?"); + if !String_IsNull(qmark) { let pathLen: uint = String_Offset(qmark, fullPath); - req.path = bux_str_slice(fullPath, 0, pathLen); + req.path = String_Slice(fullPath, 0, pathLen); let qsStart: uint = pathLen + 1; - let fullLen: uint = bux_strlen(fullPath); + let fullLen: uint = String_Len(fullPath); if qsStart < fullLen { - let qs: String = bux_str_slice(fullPath, qsStart, fullLen - qsStart); - Query_Parse(&req, qs); + let qs: String = String_Slice(fullPath, qsStart, fullLen - qsStart); + req.query = Query_Parse(qs); } } else { req.path = fullPath; @@ -314,26 +317,20 @@ func Request_Parse(raw: String) -> Request { } // Parse headers - let cap: int = lineCount as int + 1; - req.headerKeys = bux_alloc(cap as uint * 8) as *String; - req.headerValues = bux_alloc(cap as uint * 8) as *String; - var i: uint = 1; while i < lineCount { - let line: String = bux_str_split_part(headerBlock, "\r\n", i); - let colonPos: String = bux_strstr(line, ": "); - if String_Len(colonPos) > 0 { + let line: String = String_SplitPart(headerBlock, "\r\n", i); + let colonPos: String = String_Find(line, ": "); + if !String_IsNull(colonPos) { let keyLen: uint = String_Offset(colonPos, line); - let key: String = bux_str_slice(line, 0, keyLen); + let key: String = String_Slice(line, 0, keyLen); let valStart: uint = keyLen + 2; - let lineLen: uint = bux_strlen(line); + let lineLen: uint = String_Len(line); var value: String = ""; if valStart < lineLen { - value = bux_str_slice(line, valStart, lineLen - valStart); + value = String_Slice(line, valStart, lineLen - valStart); } - req.headerKeys[req.headerCount] = key; - req.headerValues[req.headerCount] = value; - req.headerCount = req.headerCount + 1; + StringMap_Set(&req.headers, key, value); } i = i + 1; } @@ -345,48 +342,23 @@ func Request_Parse(raw: String) -> Request { // Path pattern matching: /users/{id} against /users/42 → extracts id=42 // ============================================================================= func Path_Match(pattern: String, path: String, req: *Request) -> bool { - // Split both by "/" - let patParts: uint = bux_str_split_count(pattern, "/"); - let pathParts: uint = bux_str_split_count(path, "/"); + let patParts: uint = String_SplitCount(pattern, "/"); + let pathParts: uint = String_SplitCount(path, "/"); if patParts != pathParts { return false; } - // Count params to allocate - var paramCount: int = 0; - var pi: uint = 0; - while pi < patParts { - let patPart: String = bux_str_split_part(pattern, "/", pi); - if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") { - paramCount = paramCount + 1; - } - pi = pi + 1; - } + req.pathParams = StringMap_New(8); - let cap: int = paramCount + 1; - req.pathParamKeys = bux_alloc(cap as uint * 8) as *String; - req.pathParamValues = bux_alloc(cap as uint * 8) as *String; - req.pathParamCount = 0; - - // Match and extract var i: uint = 0; while i < patParts { - let patPart: String = bux_str_split_part(pattern, "/", i); - let pathPart: String = bux_str_split_part(path, "/", i); + let patPart: String = String_SplitPart(pattern, "/", i); + let pathPart: String = String_SplitPart(path, "/", i); if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") { - // Extract param name from {name} - let braceOpen: uint = 1; // skip "{" - let braceClose: String = bux_strstr(patPart, "}"); - var nameLen: uint = 0; - if String_Len(braceClose) > 0 { - nameLen = String_Offset(braceClose, patPart) - 1; - } - let name: String = bux_str_slice(patPart, braceOpen, nameLen); - req.pathParamKeys[req.pathParamCount] = name; - req.pathParamValues[req.pathParamCount] = pathPart; - req.pathParamCount = req.pathParamCount + 1; + let nameLen: uint = String_Len(patPart) - 2; + let name: String = String_Slice(patPart, 1, nameLen); + StringMap_Set(&req.pathParams, name, pathPart); } else { - // Literal match if !String_Eq(patPart, pathPart) { return false; } } i = i + 1; @@ -396,14 +368,16 @@ func Path_Match(pattern: String, path: String, req: *Request) -> bool { } // ============================================================================= -// App constructor +// App — server instance // ============================================================================= +struct App { + port: int, + threadCount: int, + serverName: String, +} + func App_New(port: int, threadCount: int) -> App { - var app: App; - app.port = port; - app.threadCount = threadCount; - app.serverName = "Boko/0.1.0 (Bux)"; - return app; + return App { port: port, threadCount: threadCount, serverName: "Boko/0.2.0 (Bux)" }; } // ============================================================================= @@ -417,19 +391,13 @@ func Boko_Router(req: Request) -> Response; func App_HandleConnection(clientFd: int) { let raw: String = Net_Recv(clientFd, 8192); if String_Len(raw) == 0 { return; } - if bux_strlen(raw) == 0 { return; } let req: Request = Request_Parse(raw); - - // Call user-defined dispatch (forward declaration — user must implement) let resp: Response = Boko_Router(req); // Log - if req.method.tag == HttpVerb_GET { Print("GET "); } - else if req.method.tag == HttpVerb_POST { Print("POST "); } - else if req.method.tag == HttpVerb_PUT { Print("PUT "); } - else if req.method.tag == HttpVerb_DELETE { Print("DELETE "); } - else { Print("? "); } + Print(HttpVerb_MethodName(req.method)); + Print(" "); Print(req.path); Print(" → "); PrintInt(resp.statusCode); @@ -457,9 +425,9 @@ func App_Worker(serverFd: int) { // ============================================================================= func App_Run(app: *App) { PrintLine("╔══════════════════════════════════════╗"); - PrintLine("║ Boko Framework v0.1.0 ║"); + PrintLine("║ Boko Framework v0.2.0 ║"); PrintLine("║ Async web framework for Bux ║"); - PrintLine("╚══════════════════════════════════════╝"); + PrintLine("╚══════════════════════════════════════════════════════╝"); PrintLine(""); let fd: int = Net_Create(); diff --git a/apps/boko-framework/src/Main.bux b/apps/boko-framework/src/Main.bux index ab58839..d8058fc 100644 --- a/apps/boko-framework/src/Main.bux +++ b/apps/boko-framework/src/Main.bux @@ -1,25 +1,22 @@ // ============================================================================= -// Boko Example App — demonstrates the framework API +// Boko Example App — demonstrates the modern framework API // ============================================================================= module Main { import Std::Io::{PrintLine, Print}; -import Std::String::{String_Eq}; +import Std::String::{ + String_Eq, String_Len, + StringBuilder, StringBuilder_New, StringBuilder_Append, + StringBuilder_Build, StringBuilder_Free +}; import Boko::{ App, App_New, App_Run, Request, Response, - Request_GetQuery, Request_GetPathParam, Response_Html, Response_Json, Response_NotFound, Response_Redirect, Path_Match, HttpVerb }; -extern func bux_strlen(s: String) -> uint; -extern func bux_sb_new(initial_cap: uint) -> *void; -extern func bux_sb_append(sb: *void, s: String); -extern func bux_sb_build(sb: *void) -> String; -extern func bux_sb_free(sb: *void); - // ============================================================================= // Boko_Router — user-defined dispatch (called by the framework) // ============================================================================= @@ -31,7 +28,7 @@ func Boko_Router(req: Request) -> Response { // --- GET /api/health --- if String_Eq(req.path, "/api/health") { - return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.1.0\"}"); + return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.2.0\"}"); } // --- GET /api/info --- @@ -41,43 +38,38 @@ func Boko_Router(req: Request) -> Response { // --- GET /hello?name=World --- if String_Eq(req.path, "/hello") { - let name: String = Request_GetQuery(&req, "name"); - if bux_strlen(name) == 0 { name = "World"; } - let sb: *void = bux_sb_new(256); - bux_sb_append(sb, "

Hello, "); - bux_sb_append(sb, name); - bux_sb_append(sb, "!

"); - let html: String = bux_sb_build(sb); - bux_sb_free(sb); + var name: String = req.GetQuery("name"); + if String_Len(name) == 0 { name = "World"; } + let html: String = f"

Hello, {name}!

"; return Response_Html(html); } // --- GET /users/{id} --- if Path_Match("/users/{id}", req.path, &req) { - let id: String = Request_GetPathParam(&req, "id"); - let sb: *void = bux_sb_new(128); - bux_sb_append(sb, "{\"id\":"); - bux_sb_append(sb, id); - bux_sb_append(sb, ",\"name\":\"User "); - bux_sb_append(sb, id); - bux_sb_append(sb, "\"}"); - let json: String = bux_sb_build(sb); - bux_sb_free(sb); + let id: String = req.GetPathParam("id"); + let sb: StringBuilder = StringBuilder_New(); + StringBuilder_Append(&sb, "{\"id\":"); + StringBuilder_Append(&sb, id); + StringBuilder_Append(&sb, ",\"name\":\"User "); + StringBuilder_Append(&sb, id); + StringBuilder_Append(&sb, "\"}"); + let json: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); return Response_Json(json); } // --- GET /posts/{postId}/comments/{commentId} --- if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) { - let pid: String = Request_GetPathParam(&req, "postId"); - let cid: String = Request_GetPathParam(&req, "commentId"); - let sb: *void = bux_sb_new(128); - bux_sb_append(sb, "{\"postId\":"); - bux_sb_append(sb, pid); - bux_sb_append(sb, ",\"commentId\":"); - bux_sb_append(sb, cid); - bux_sb_append(sb, ",\"text\":\"Great post!\"}"); - let json: String = bux_sb_build(sb); - bux_sb_free(sb); + let pid: String = req.GetPathParam("postId"); + let cid: String = req.GetPathParam("commentId"); + let sb: StringBuilder = StringBuilder_New(); + StringBuilder_Append(&sb, "{\"postId\":"); + StringBuilder_Append(&sb, pid); + StringBuilder_Append(&sb, ",\"commentId\":"); + StringBuilder_Append(&sb, cid); + StringBuilder_Append(&sb, ",\"text\":\"Great post!\"}"); + let json: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); return Response_Json(json); } @@ -88,12 +80,12 @@ func Boko_Router(req: Request) -> Response { // --- POST /api/echo --- if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST { - let sb: *void = bux_sb_new(256); - bux_sb_append(sb, "{\"echo\":"); - bux_sb_append(sb, req.body); - bux_sb_append(sb, "}"); - let json: String = bux_sb_build(sb); - bux_sb_free(sb); + let sb: StringBuilder = StringBuilder_New(); + StringBuilder_Append(&sb, "{\"echo\":"); + StringBuilder_Append(&sb, req.body); + StringBuilder_Append(&sb, "}"); + let json: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); return Response_Json(json); } diff --git a/apps/jwt-pitbul/README.md b/apps/jwt-pitbul/README.md index 24a5e25..76efb4c 100644 --- a/apps/jwt-pitbul/README.md +++ b/apps/jwt-pitbul/README.md @@ -4,12 +4,14 @@ A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL. +This version (0.2.0) is rewritten with modern Bux: algebraic enums, generic `Array`, methods on `JwtAlg`, string interpolation, `for` loops, and structured `Result`/`Option`-style error handling. + ## Quick Start ```bash cd apps/jwt-pitbul ../../buxc build -./jwt-pitbul +./build/jwt-pitbul ``` ## Commands diff --git a/apps/jwt-pitbul/bux.toml b/apps/jwt-pitbul/bux.toml index 50827c4..75e9ba2 100644 --- a/apps/jwt-pitbul/bux.toml +++ b/apps/jwt-pitbul/bux.toml @@ -1,6 +1,6 @@ [Package] Name = "jwt-pitbul" -Version = "0.1.0" +Version = "0.2.0" Type = "bin" Description = "JWT CLI tool — encode, decode, verify, and key generation" diff --git a/apps/jwt-pitbul/src/Main.bux b/apps/jwt-pitbul/src/Main.bux index 36f7d77..95f865a 100644 --- a/apps/jwt-pitbul/src/Main.bux +++ b/apps/jwt-pitbul/src/Main.bux @@ -1,11 +1,17 @@ // ============================================================================= // jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen) -// Built with Bux + Std::Crypto +// Rewritten with modern Bux: algebraic enums, methods, string interpolation, +// for-in loops, generic arrays, and structured error handling. // ============================================================================= module Main { import Std::Io::{PrintLine, Print, PrintInt}; -import Std::String::{String_Len, String_Eq, String_Contains}; +import Std::String::{ + String_Len, String_Eq, + String_SplitCount, String_SplitPart, + StringBuilder, StringBuilder_New, StringBuilder_Append, + StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free +}; import Std::Crypto::Jwt::{ JwtAlg, Jwt_MakeHeader, @@ -22,24 +28,191 @@ extern func bux_argv(index: int) -> String; extern func bux_alloc(size: uint) -> *void; extern func bux_read_file(path: String) -> String; extern func bux_file_exists(path: String) -> int; -extern func bux_str_split_part(s: String, delim: String, index: uint) -> String; -extern func bux_str_split_count(s: String, delim: String) -> uint; extern func bux_base64_encode(data: String, len: int) -> String; +// ============================================================================= +// Constants +// ============================================================================= +const AppName: String = "jwt-pitbul"; +const Version: String = "0.2.0"; + +// ============================================================================= +// Algebraic enums for optionals and results +// ============================================================================= +enum AlgOption { + Some(JwtAlg), + None, +} + +func AlgOption_MakeSome(value: JwtAlg) -> AlgOption { + let o: AlgOption = AlgOption { tag: AlgOption_Some }; + o.data.Some_0 = value; + return o; +} + +func AlgOption_MakeNone() -> AlgOption { + return AlgOption { tag: AlgOption_None }; +} + +enum CmdOption { + Some(Cmd), + None, +} + +func CmdOption_MakeSome(value: Cmd) -> CmdOption { + let o: CmdOption = CmdOption { tag: CmdOption_Some }; + o.data.Some_0 = value; + return o; +} + +func CmdOption_MakeNone() -> CmdOption { + return CmdOption { tag: CmdOption_None }; +} + +enum KeyTypeOption { + Some(KeyType), + None, +} + +func KeyTypeOption_MakeSome(value: KeyType) -> KeyTypeOption { + let o: KeyTypeOption = KeyTypeOption { tag: KeyTypeOption_Some }; + o.data.Some_0 = value; + return o; +} + +func KeyTypeOption_MakeNone() -> KeyTypeOption { + return KeyTypeOption { tag: KeyTypeOption_None }; +} + +enum KeyResult { + Ok(String), + Err(String), +} + +func KeyResult_MakeOk(value: String) -> KeyResult { + let r: KeyResult = KeyResult { tag: KeyResult_Ok }; + r.data.Ok_0 = value; + return r; +} + +func KeyResult_MakeErr(msg: String) -> KeyResult { + let r: KeyResult = KeyResult { tag: KeyResult_Err }; + r.data.Err_0 = msg; + return r; +} + +enum ExitResult { + Ok(int), + Err(String), +} + +func ExitResult_MakeOk(value: int) -> ExitResult { + let r: ExitResult = ExitResult { tag: ExitResult_Ok }; + r.data.Ok_0 = value; + return r; +} + +func ExitResult_MakeErr(msg: String) -> ExitResult { + let r: ExitResult = ExitResult { tag: ExitResult_Err }; + r.data.Err_0 = msg; + return r; +} + +// ============================================================================= +// Commands +// ============================================================================= +enum Cmd { + Sign, + Verify, + Decode, + Keygen, + Help, +} + +func ParseCmd(name: String) -> CmdOption { + if String_Eq(name, "sign") { return CmdOption_MakeSome(Cmd { tag: Cmd_Sign }); } + if String_Eq(name, "verify") { return CmdOption_MakeSome(Cmd { tag: Cmd_Verify }); } + if String_Eq(name, "decode") { return CmdOption_MakeSome(Cmd { tag: Cmd_Decode }); } + if String_Eq(name, "keygen") { return CmdOption_MakeSome(Cmd { tag: Cmd_Keygen }); } + if String_Eq(name, "help") || String_Eq(name, "--help") || String_Eq(name, "-h") { + return CmdOption_MakeSome(Cmd { tag: Cmd_Help }); + } + return CmdOption_MakeNone(); +} + +// ============================================================================= +// Key type generation +// ============================================================================= +enum KeyType { + Rsa, + Ecdsa, + Ed25519, +} + +func ParseKeyType(name: String) -> KeyTypeOption { + if String_Eq(name, "rsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Rsa }); } + if String_Eq(name, "ecdsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ecdsa }); } + if String_Eq(name, "ed25519") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ed25519 }); } + return KeyTypeOption_MakeNone(); +} + +// ============================================================================= +// Extend JwtAlg with parsing / introspection methods +// ============================================================================= +extend JwtAlg { + func Name(self: JwtAlg) -> String { + match self { + JwtAlg::HS256 => "HS256", + JwtAlg::HS384 => "HS384", + JwtAlg::HS512 => "HS512", + JwtAlg::RS256 => "RS256", + JwtAlg::RS384 => "RS384", + JwtAlg::RS512 => "RS512", + JwtAlg::ES256 => "ES256", + JwtAlg::ES384 => "ES384", + JwtAlg::EdDSA => "EdDSA", + _ => "HS256", + } + } + + func IsHmac(self: JwtAlg) -> bool { + return self.tag == JwtAlg_HS256 || self.tag == JwtAlg_HS384 || self.tag == JwtAlg_HS512; + } + + func NeedsPemFile(self: JwtAlg) -> bool { + return self.tag == JwtAlg_RS256 || self.tag == JwtAlg_RS384 || self.tag == JwtAlg_RS512 || + self.tag == JwtAlg_ES256 || self.tag == JwtAlg_ES384; + } + +} + +func ParseAlg(name: String) -> AlgOption { + if String_Eq(name, "HS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS256 }); } + if String_Eq(name, "HS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS384 }); } + if String_Eq(name, "HS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS512 }); } + if String_Eq(name, "RS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS256 }); } + if String_Eq(name, "RS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS384 }); } + if String_Eq(name, "RS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS512 }); } + if String_Eq(name, "ES256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES256 }); } + if String_Eq(name, "ES384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES384 }); } + if String_Eq(name, "EdDSA") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_EdDSA }); } + return AlgOption_MakeNone(); +} + // ============================================================================= // Help / Usage // ============================================================================= func PrintUsage() { PrintLine("╔══════════════════════════════════════════════════════╗"); - PrintLine("║ jwt-pitbul — JWT CLI Tool v0.1.0 ║"); + PrintLine(f"║ {AppName} — JWT CLI Tool v{Version} ║"); PrintLine("║ Sign, verify, decode JSON Web Tokens ║"); PrintLine("╚══════════════════════════════════════════════════════╝"); PrintLine(""); PrintLine("Usage:"); - PrintLine(" jwt-pitbul sign "); - PrintLine(" jwt-pitbul verify "); - PrintLine(" jwt-pitbul decode "); - PrintLine(" jwt-pitbul keygen "); + PrintLine(f" {AppName} sign "); + PrintLine(f" {AppName} verify "); + PrintLine(f" {AppName} decode "); + PrintLine(f" {AppName} keygen "); PrintLine(""); PrintLine("Commands:"); PrintLine(" sign Create a signed JWT from claims JSON"); @@ -60,130 +233,109 @@ func PrintUsage() { PrintLine(" EdDSA: base64-encoded 32-byte raw key"); PrintLine(""); PrintLine("Key generation:"); - PrintLine(" jwt-pitbul keygen rsa # RSA 2048-bit (PEM)"); - PrintLine(" jwt-pitbul keygen ecdsa # ECDSA P-256 (PEM)"); - PrintLine(" jwt-pitbul keygen ed25519 # Ed25519 (raw base64)"); + PrintLine(f" {AppName} keygen rsa # RSA 2048-bit (PEM)"); + PrintLine(f" {AppName} keygen ecdsa # ECDSA P-256 (PEM)"); + PrintLine(f" {AppName} keygen ed25519 # Ed25519 (raw base64)"); PrintLine(""); PrintLine("Examples:"); - PrintLine(" jwt-pitbul sign HS256 'my-secret' '{\"sub\":\"123\"}'"); - PrintLine(" jwt-pitbul verify eyJh... HS256 'my-secret'"); - PrintLine(" jwt-pitbul decode eyJh..."); - PrintLine(" jwt-pitbul keygen ed25519"); + Print(f" {AppName} sign HS256 'my-secret' "); + PrintLine("'{\"sub\":\"123\"}'"); + PrintLine(f" {AppName} verify eyJh... HS256 'my-secret'"); + PrintLine(f" {AppName} decode eyJh..."); + PrintLine(f" {AppName} keygen ed25519"); } // ============================================================================= -// Algorithm name → JwtAlg enum +// Key resolution — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through // ============================================================================= -func ParseAlg(name: String) -> JwtAlg { - if String_Eq(name, "HS256") { return JwtAlg { tag: JwtAlg_HS256 }; } - if String_Eq(name, "HS384") { return JwtAlg { tag: JwtAlg_HS384 }; } - if String_Eq(name, "HS512") { return JwtAlg { tag: JwtAlg_HS512 }; } - if String_Eq(name, "RS256") { return JwtAlg { tag: JwtAlg_RS256 }; } - if String_Eq(name, "RS384") { return JwtAlg { tag: JwtAlg_RS384 }; } - if String_Eq(name, "RS512") { return JwtAlg { tag: JwtAlg_RS512 }; } - if String_Eq(name, "ES256") { return JwtAlg { tag: JwtAlg_ES256 }; } - if String_Eq(name, "ES384") { return JwtAlg { tag: JwtAlg_ES384 }; } - if String_Eq(name, "EdDSA") { return JwtAlg { tag: JwtAlg_EdDSA }; } - return JwtAlg { tag: JwtAlg_HS256 }; -} - -// ============================================================================= -// Resolve key — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through -// ============================================================================= -func ResolveKey(alg: JwtAlg, keyArg: String) -> String { - // RSA and ECDSA: key is a path to PEM file - if alg.tag == JwtAlg_RS256 || alg.tag == JwtAlg_RS384 || alg.tag == JwtAlg_RS512 || - alg.tag == JwtAlg_ES256 || alg.tag == JwtAlg_ES384 { - if bux_file_exists(keyArg) != 0 { - let pem: String = bux_read_file(keyArg); - if String_Len(pem) > 0 && String_Len(pem) > 0 { - return pem; - } - Print("ERROR: could not read PEM file: "); - PrintLine(keyArg); - return ""; - } - Print("ERROR: PEM file not found: "); - PrintLine(keyArg); - return ""; +func ResolveKey(alg: JwtAlg, keyArg: String) -> KeyResult { + if !alg.NeedsPemFile() { + return KeyResult_MakeOk(keyArg); } - // HMAC and EdDSA: key is used directly - return keyArg; -} + if bux_file_exists(keyArg) == 0 { + return KeyResult_MakeErr(f"ERROR: PEM file not found: {keyArg}"); + } -// ============================================================================= -// Check if algorithm is symmetric (HMAC) or asymmetric -// ============================================================================= -func IsSymmetric(alg: JwtAlg) -> bool { - return alg.tag == JwtAlg_HS256 || alg.tag == JwtAlg_HS384 || alg.tag == JwtAlg_HS512; + let pem: String = bux_read_file(keyArg); + if String_Len(pem) == 0 { + return KeyResult_MakeErr(f"ERROR: could not read PEM file: {keyArg}"); + } + return KeyResult_MakeOk(pem); } // ============================================================================= // Command: sign // ============================================================================= -func CmdSign(algName: String, keyArg: String, claimsJson: String) -> int { - let alg: JwtAlg = ParseAlg(algName); - let key: String = ResolveKey(alg, keyArg); - if String_Len(key) == 0 { - return 1; +func CmdSign(algName: String, keyArg: String, claimsJson: String) -> ExitResult { + let algOpt: AlgOption = ParseAlg(algName); + if algOpt.tag != AlgOption_Some { + return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'"); } + let alg: JwtAlg = algOpt.data.Some_0; + + let keyRes: KeyResult = ResolveKey(alg, keyArg); + if keyRes.tag != KeyResult_Ok { + return ExitResult_MakeErr(keyRes.data.Err_0); + } + let key: String = keyRes.data.Ok_0; let header: String = Jwt_MakeHeader(alg); let token: String = Jwt_Encode(header, claimsJson, alg, key); if String_Len(token) == 0 { - PrintLine("ERROR: signing failed"); - return 1; + return ExitResult_MakeErr("ERROR: signing failed"); } PrintLine(token); - return 0; + return ExitResult_MakeOk(0); } // ============================================================================= // Command: verify // ============================================================================= -func CmdVerify(token: String, algName: String, keyArg: String) -> int { - let alg: JwtAlg = ParseAlg(algName); - let key: String = ResolveKey(alg, keyArg); - if String_Len(key) == 0 { - return 1; +func CmdVerify(token: String, algName: String, keyArg: String) -> ExitResult { + let algOpt: AlgOption = ParseAlg(algName); + if algOpt.tag != AlgOption_Some { + return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'"); } + let alg: JwtAlg = algOpt.data.Some_0; + + let keyRes: KeyResult = ResolveKey(alg, keyArg); + if keyRes.tag != KeyResult_Ok { + return ExitResult_MakeErr(keyRes.data.Err_0); + } + let key: String = keyRes.data.Ok_0; var header: String = ""; var payload: String = ""; - let ok: bool = Jwt_Decode(token, alg, key, &header, &payload); - - if ok { - PrintLine("✓ Signature valid"); - PrintLine(""); - PrintLine("Header:"); - PrintLine(header); - PrintLine(""); - PrintLine("Payload:"); - PrintLine(payload); - return 0; + if !Jwt_Decode(token, alg, key, &header, &payload) { + return ExitResult_MakeErr("✗ Signature INVALID (or malformed token)"); } - PrintLine("✗ Signature INVALID (or malformed token)"); - return 1; + PrintLine("✓ Signature valid"); + PrintLine(""); + PrintLine("Header:"); + PrintLine(header); + PrintLine(""); + PrintLine("Payload:"); + PrintLine(payload); + return ExitResult_MakeOk(0); } // ============================================================================= // Command: decode (no verification) // ============================================================================= -func CmdDecode(token: String) -> int { - let partCount: uint = bux_str_split_count(token, "."); +func CmdDecode(token: String) -> ExitResult { + let partCount: uint = String_SplitCount(token, "."); if partCount != 3 { - PrintLine("ERROR: not a valid JWT (expected 3 parts)"); - return 1; + return ExitResult_MakeErr("ERROR: not a valid JWT (expected 3 parts)"); } - let headerB64: String = bux_str_split_part(token, ".", 0); - let payloadB64: String = bux_str_split_part(token, ".", 1); - let sigB64: String = bux_str_split_part(token, ".", 2); + let headerB64: String = String_SplitPart(token, ".", 0); + let payloadB64: String = String_SplitPart(token, ".", 1); + let sigB64: String = String_SplitPart(token, ".", 2); let headerJson: String = Base64URL_Decode(headerB64); let payloadJson: String = Base64URL_Decode(payloadB64); @@ -199,52 +351,84 @@ func CmdDecode(token: String) -> int { Print("Signature (base64url): "); PrintLine(sigB64); - return 0; + return ExitResult_MakeOk(0); } // ============================================================================= // Command: keygen // ============================================================================= -func CmdKeygen(keyType: String) -> int { - if String_Eq(keyType, "ed25519") { +func CmdKeygen(keyType: String) -> ExitResult { + let ktOpt: KeyTypeOption = ParseKeyType(keyType); + if ktOpt.tag != KeyTypeOption_Some { + return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'. Use: rsa, ecdsa, ed25519"); + } + let kt: KeyType = ktOpt.data.Some_0; + + if kt.tag == KeyType_Ed25519 { let pubBuf: *void = bux_alloc(32); let priv: *void = bux_alloc(32); if !Ed25519_Keypair(pubBuf, priv) { - PrintLine("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)"); - return 1; + return ExitResult_MakeErr("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)"); } let pubB64: String = bux_base64_encode(pubBuf as String, 32); let privB64: String = bux_base64_encode(priv as String, 32); PrintLine("Ed25519 keypair (base64):"); - Print(" Public: "); - PrintLine(pubB64); - Print(" Private: "); - PrintLine(privB64); - return 0; + PrintLine(f" Public: {pubB64}"); + PrintLine(f" Private: {privB64}"); + return ExitResult_MakeOk(0); } - - if String_Eq(keyType, "rsa") { + if kt.tag == KeyType_Rsa { PrintLine("RSA key generation requires OpenSSL CLI:"); PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048"); PrintLine(" openssl rsa -in private.pem -pubout -out public.pem"); PrintLine(""); PrintLine("(Key generation from within Bux requires PEM write support — coming soon)"); - return 0; + return ExitResult_MakeOk(0); } - - if String_Eq(keyType, "ecdsa") { + if kt.tag == KeyType_Ecdsa { PrintLine("ECDSA key generation requires OpenSSL CLI:"); PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem"); PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem"); PrintLine(""); PrintLine("(Key generation from within Bux requires PEM write support — coming soon)"); - return 0; + return ExitResult_MakeOk(0); } + return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'"); +} - Print("ERROR: unknown key type '"); - Print(keyType); - PrintLine("'. Use: rsa, ecdsa, ed25519"); - return 1; +// ============================================================================= +// Dispatch a parsed command to its handler +// ============================================================================= +func RunCommand(cmd: Cmd, args: Array) -> ExitResult { + if cmd.tag == Cmd_Help { + PrintUsage(); + return ExitResult_MakeOk(0); + } + if cmd.tag == Cmd_Sign { + if args.len < 5 { + return ExitResult_MakeErr("ERROR: 'sign' requires: "); + } + return CmdSign(args[2], args[3], args[4]); + } + if cmd.tag == Cmd_Verify { + if args.len < 5 { + return ExitResult_MakeErr("ERROR: 'verify' requires: "); + } + return CmdVerify(args[2], args[3], args[4]); + } + if cmd.tag == Cmd_Decode { + if args.len < 3 { + return ExitResult_MakeErr("ERROR: 'decode' requires: "); + } + return CmdDecode(args[2]); + } + if cmd.tag == Cmd_Keygen { + if args.len < 3 { + return ExitResult_MakeErr("ERROR: 'keygen' requires: "); + } + return CmdKeygen(args[2]); + } + return ExitResult_MakeErr("ERROR: unhandled command"); } // ============================================================================= @@ -253,71 +437,32 @@ func CmdKeygen(keyType: String) -> int { func Main() -> int { let argc: int = bux_argc(); - if argc < 2 { - PrintUsage(); - return 0; + // Collect CLI arguments into a generic Array + let args: Array = Array_New(argc as uint); + for i in 0..argc { + Array_Push(&args, bux_argv(i)); } - // Collect arguments - let args: *String = bux_alloc(argc as uint * 8) as *String; - var i: int = 0; - while i < argc { - args[i] = bux_argv(i); - i = i + 1; + if args.len < 2 { + PrintUsage(); + return 0; } let command: String = args[1]; - - if String_Eq(command, "help") || String_Eq(command, "--help") || String_Eq(command, "-h") { - PrintUsage(); - return 0; + let cmdOpt: CmdOption = ParseCmd(command); + if cmdOpt.tag != CmdOption_Some { + PrintLine(f"ERROR: unknown command '{command}'"); + PrintLine(f"Run '{AppName} help' for usage."); + return 1; } + let cmd: Cmd = cmdOpt.data.Some_0; - // --- sign --- - if String_Eq(command, "sign") { - if argc < 5 { - PrintLine("ERROR: 'sign' requires: "); - PrintLine(" jwt-pitbul sign HS256 'secret' '{\"sub\":\"123\"}'"); - return 1; - } - return CmdSign(args[2], args[3], args[4]); + let result: ExitResult = RunCommand(cmd, args); + if result.tag == ExitResult_Err { + PrintLine(result.data.Err_0); + return 1; } - - // --- verify --- - if String_Eq(command, "verify") { - if argc < 5 { - PrintLine("ERROR: 'verify' requires: "); - PrintLine(" jwt-pitbul verify eyJh... HS256 'secret'"); - return 1; - } - return CmdVerify(args[2], args[3], args[4]); - } - - // --- decode --- - if String_Eq(command, "decode") { - if argc < 3 { - PrintLine("ERROR: 'decode' requires: "); - PrintLine(" jwt-pitbul decode eyJh..."); - return 1; - } - return CmdDecode(args[2]); - } - - // --- keygen --- - if String_Eq(command, "keygen") { - if argc < 3 { - PrintLine("ERROR: 'keygen' requires: "); - PrintLine(" jwt-pitbul keygen ed25519"); - return 1; - } - return CmdKeygen(args[3]); - } - - Print("ERROR: unknown command '"); - Print(command); - PrintLine("'"); - PrintLine("Run 'jwt-pitbul help' for usage."); - return 1; + return result.data.Ok_0; } } // module Main