feat: lifetime elision, tooling CI, registry, and LSP locals
Ship the QUALITY_PLAN stretch from ownership through ecosystem: C.1 lifetime elision (bootstrap + selfhost), bux fmt/test/doc CI hooks, stdlib goldens, package registry (bux search/add), and LSP 0.4 position-sensitive locals with inferred let types. Full-tree format pass plus Map/Set remove double-free fix.
This commit is contained in:
+116
-116
@@ -5,143 +5,143 @@
|
||||
// =============================================================================
|
||||
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_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};
|
||||
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_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};
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Methods
|
||||
// =============================================================================
|
||||
enum HttpVerb {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
}
|
||||
// =============================================================================
|
||||
// HTTP Methods
|
||||
// =============================================================================
|
||||
enum HttpVerb {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
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_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 };
|
||||
}
|
||||
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
|
||||
// =============================================================================
|
||||
struct Request {
|
||||
method: HttpVerb,
|
||||
path: String,
|
||||
body: String,
|
||||
headers: StringMap<String>,
|
||||
query: StringMap<String>,
|
||||
pathParams: StringMap<String>,
|
||||
}
|
||||
// =============================================================================
|
||||
// Request — parsed incoming HTTP request
|
||||
// =============================================================================
|
||||
struct Request {
|
||||
method: HttpVerb,
|
||||
path: String,
|
||||
body: String,
|
||||
headers: StringMap<String>,
|
||||
query: StringMap<String>,
|
||||
pathParams: StringMap<String>,
|
||||
}
|
||||
|
||||
extend Request {
|
||||
func GetHeader(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.headers, name) {
|
||||
return StringMap_Get<String>(&self.headers, name);
|
||||
extend Request {
|
||||
func GetHeader(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.headers, name) {
|
||||
return StringMap_Get<String>(&self.headers, name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
func GetQuery(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.query, name) {
|
||||
return StringMap_Get<String>(&self.query, name);
|
||||
func GetQuery(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.query, name) {
|
||||
return StringMap_Get<String>(&self.query, name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
func HasQuery(self: Request, name: String) -> bool {
|
||||
return StringMap_Has<String>(&self.query, name);
|
||||
}
|
||||
|
||||
func GetPathParam(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.pathParams, name) {
|
||||
return StringMap_Get<String>(&self.pathParams, name);
|
||||
func HasQuery(self: Request, name: String) -> bool {
|
||||
return StringMap_Has<String>(&self.query, name);
|
||||
}
|
||||
|
||||
func GetPathParam(self: Request, name: String) -> String {
|
||||
if StringMap_Has<String>(&self.pathParams, name) {
|
||||
return StringMap_Get<String>(&self.pathParams, name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Response — outgoing HTTP response
|
||||
// =============================================================================
|
||||
struct Response {
|
||||
statusCode: int,
|
||||
contentType: String,
|
||||
body: String,
|
||||
extraHeaders: String,
|
||||
}
|
||||
// =============================================================================
|
||||
// Response — outgoing HTTP response
|
||||
// =============================================================================
|
||||
struct Response {
|
||||
statusCode: int,
|
||||
contentType: String,
|
||||
body: String,
|
||||
extraHeaders: String,
|
||||
}
|
||||
|
||||
// --- Constructors ---
|
||||
func Response_New(status: int, contentType: String, body: String) -> Response {
|
||||
return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" };
|
||||
}
|
||||
// --- Constructors ---
|
||||
func Response_New(status: int, contentType: String, body: String) -> Response {
|
||||
return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" };
|
||||
}
|
||||
|
||||
func Response_Ok(body: String) -> Response {
|
||||
return Response_New(200, "text/html; charset=utf-8", body);
|
||||
}
|
||||
func Response_Ok(body: String) -> Response {
|
||||
return Response_New(200, "text/html; charset=utf-8", body);
|
||||
}
|
||||
|
||||
func Response_Html(html: String) -> Response {
|
||||
return Response_New(200, "text/html; charset=utf-8", html);
|
||||
}
|
||||
func Response_Html(html: String) -> Response {
|
||||
return Response_New(200, "text/html; charset=utf-8", html);
|
||||
}
|
||||
|
||||
func Response_Json(json: String) -> Response {
|
||||
return Response_New(200, "application/json; charset=utf-8", json);
|
||||
}
|
||||
func Response_Json(json: String) -> Response {
|
||||
return Response_New(200, "application/json; charset=utf-8", json);
|
||||
}
|
||||
|
||||
func Response_Text(text: String) -> Response {
|
||||
return Response_New(200, "text/plain; charset=utf-8", text);
|
||||
}
|
||||
func Response_Text(text: String) -> Response {
|
||||
return Response_New(200, "text/plain; charset=utf-8", text);
|
||||
}
|
||||
|
||||
func Response_Redirect(url: String) -> Response {
|
||||
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_Redirect(url: String) -> Response {
|
||||
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\"}");
|
||||
}
|
||||
func Response_NotFound() -> Response {
|
||||
return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||
}
|
||||
|
||||
func Response_Error(status: int, message: String) -> Response {
|
||||
let sb: StringBuilder = StringBuilder_New();
|
||||
StringBuilder_Append(&sb, "{\"error\":\"");
|
||||
StringBuilder_Append(&sb, message);
|
||||
StringBuilder_Append(&sb, "\"}");
|
||||
func Response_Error(status: int, message: String) -> Response {
|
||||
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);
|
||||
|
||||
@@ -3,56 +3,56 @@
|
||||
// =============================================================================
|
||||
module Main {
|
||||
|
||||
import Std::Io::{PrintLine, Print};
|
||||
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,
|
||||
Response_Html, Response_Json, Response_NotFound, Response_Redirect,
|
||||
Path_Match,
|
||||
HttpVerb
|
||||
};
|
||||
import Std::Io::{PrintLine, Print};
|
||||
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,
|
||||
Response_Html, Response_Json, Response_NotFound, Response_Redirect,
|
||||
Path_Match,
|
||||
HttpVerb
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Boko_Router — user-defined dispatch (called by the framework)
|
||||
// =============================================================================
|
||||
func Boko_Router(req: Request) -> Response {
|
||||
// --- GET / ---
|
||||
if String_Eq(req.path, "/") && req.method.tag == HttpVerb_GET {
|
||||
return Response_Html(PageHome());
|
||||
}
|
||||
// =============================================================================
|
||||
// Boko_Router — user-defined dispatch (called by the framework)
|
||||
// =============================================================================
|
||||
func Boko_Router(req: Request) -> Response {
|
||||
// --- GET / ---
|
||||
if String_Eq(req.path, "/") && req.method.tag == HttpVerb_GET {
|
||||
return Response_Html(PageHome());
|
||||
}
|
||||
|
||||
// --- GET /api/health ---
|
||||
if String_Eq(req.path, "/api/health") {
|
||||
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.2.0\"}");
|
||||
}
|
||||
// --- GET /api/health ---
|
||||
if String_Eq(req.path, "/api/health") {
|
||||
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.2.0\"}");
|
||||
}
|
||||
|
||||
// --- GET /api/info ---
|
||||
if String_Eq(req.path, "/api/info") {
|
||||
return Response_Json("{\"name\":\"Boko\",\"language\":\"Bux\",\"inspiration\":\"FastAPI\",\"features\":[\"routing\",\"path-params\",\"query-params\",\"json\"]}");
|
||||
}
|
||||
// --- GET /api/info ---
|
||||
if String_Eq(req.path, "/api/info") {
|
||||
return Response_Json("{\"name\":\"Boko\",\"language\":\"Bux\",\"inspiration\":\"FastAPI\",\"features\":[\"routing\",\"path-params\",\"query-params\",\"json\"]}");
|
||||
}
|
||||
|
||||
// --- GET /hello?name=World ---
|
||||
if String_Eq(req.path, "/hello") {
|
||||
var name: String = req.GetQuery("name");
|
||||
if String_Len(name) == 0 { name = "World"; }
|
||||
let html: String = f"<h1>Hello, {name}!</h1>";
|
||||
return Response_Html(html);
|
||||
}
|
||||
// --- GET /hello?name=World ---
|
||||
if String_Eq(req.path, "/hello") {
|
||||
var name: String = req.GetQuery("name");
|
||||
if String_Len(name) == 0 { name = "World"; }
|
||||
let html: String = f"<h1>Hello, {name}!</h1>";
|
||||
return Response_Html(html);
|
||||
}
|
||||
|
||||
// --- GET /users/{id} ---
|
||||
if Path_Match("/users/{id}", req.path, &req) {
|
||||
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, "\"}");
|
||||
// --- GET /users/{id} ---
|
||||
if Path_Match("/users/{id}", req.path, &req) {
|
||||
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);
|
||||
@@ -98,46 +98,46 @@ func Boko_Router(req: Request) -> Response {
|
||||
// =============================================================================
|
||||
func PageHome() -> String {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Boko Framework</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:system-ui,sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
||||
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:3rem;max-width:560px;width:100%}
|
||||
h1{color:#58a6ff;font-size:2rem;margin-bottom:.5rem}
|
||||
.tag{color:#8b949e;font-size:.9rem;margin-bottom:2rem}
|
||||
h3{color:#d2a8ff;margin:1.5rem 0 .75rem}
|
||||
.ep{display:flex;gap:1rem;padding:.35rem 0;font-family:monospace;font-size:.9rem}
|
||||
.ep .m{color:#3fb950;font-weight:bold;min-width:52px}
|
||||
.ep .p{color:#c9d1d9}
|
||||
.ep .d{color:#484f58;margin-left:auto}
|
||||
a{color:#58a6ff}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>⚡ Boko</h1>
|
||||
<p class="tag">Async web framework for Bux — inspired by FastAPI</p>
|
||||
<h3>Try it</h3>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/hello?name=Bux">/hello?name=Bux</a></span><span class="d">query param</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/users/42">/users/42</a></span><span class="d">path param</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/posts/7/comments/3">/posts/7/comments/3</a></span><span class="d">multi params</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/redirect">/redirect</a></span><span class="d">302 → /</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/health">/api/health</a></span><span class="d">JSON</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/info">/api/info</a></span><span class="d">JSON</span></div>
|
||||
<h3>Features</h3>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Path routing with {params}</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Query parameter extraction</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">JSON / HTML / Text responses</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Multi-threaded (configurable)</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Redirects (302)</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">POST body access</span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Boko Framework</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:system-ui,sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
||||
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:3rem;max-width:560px;width:100%}
|
||||
h1{color:#58a6ff;font-size:2rem;margin-bottom:.5rem}
|
||||
.tag{color:#8b949e;font-size:.9rem;margin-bottom:2rem}
|
||||
h3{color:#d2a8ff;margin:1.5rem 0 .75rem}
|
||||
.ep{display:flex;gap:1rem;padding:.35rem 0;font-family:monospace;font-size:.9rem}
|
||||
.ep .m{color:#3fb950;font-weight:bold;min-width:52px}
|
||||
.ep .p{color:#c9d1d9}
|
||||
.ep .d{color:#484f58;margin-left:auto}
|
||||
a{color:#58a6ff}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>⚡ Boko</h1>
|
||||
<p class="tag">Async web framework for Bux — inspired by FastAPI</p>
|
||||
<h3>Try it</h3>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/hello?name=Bux">/hello?name=Bux</a></span><span class="d">query param</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/users/42">/users/42</a></span><span class="d">path param</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/posts/7/comments/3">/posts/7/comments/3</a></span><span class="d">multi params</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/redirect">/redirect</a></span><span class="d">302 → /</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/health">/api/health</a></span><span class="d">JSON</span></div>
|
||||
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/info">/api/info</a></span><span class="d">JSON</span></div>
|
||||
<h3>Features</h3>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Path routing with {params}</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Query parameter extraction</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">JSON / HTML / Text responses</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Multi-threaded (configurable)</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">Redirects (302)</span></div>
|
||||
<div class="ep"><span class="m">✓</span><span class="p">POST body access</span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
+425
-425
@@ -5,464 +5,464 @@
|
||||
// =============================================================================
|
||||
module Main {
|
||||
|
||||
import Std::Io::{PrintLine, Print, PrintInt};
|
||||
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,
|
||||
Jwt_Encode,
|
||||
Jwt_Decode,
|
||||
Jwt_EncodeHS256, Jwt_EncodeHS384, Jwt_EncodeHS512,
|
||||
Jwt_EncodeRS256, Jwt_EncodeES256, Jwt_EncodeEdDSA
|
||||
};
|
||||
import Std::Crypto::Base64::{Base64URL_Decode, Base64_Encode};
|
||||
import Std::Crypto::Ed25519::{Ed25519_Keypair};
|
||||
import Std::Io::{PrintLine, Print, PrintInt};
|
||||
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,
|
||||
Jwt_Encode,
|
||||
Jwt_Decode,
|
||||
Jwt_EncodeHS256, Jwt_EncodeHS384, Jwt_EncodeHS512,
|
||||
Jwt_EncodeRS256, Jwt_EncodeES256, Jwt_EncodeEdDSA
|
||||
};
|
||||
import Std::Crypto::Base64::{Base64URL_Decode, Base64_Encode};
|
||||
import Std::Crypto::Ed25519::{Ed25519_Keypair};
|
||||
|
||||
extern func bux_argc() -> int;
|
||||
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_base64_encode(data: String, len: int) -> String;
|
||||
extern func bux_argc() -> int;
|
||||
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_base64_encode(data: String, len: int) -> String;
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
const AppName: String = "jwt-pitbul";
|
||||
const Version: String = "0.2.0";
|
||||
// =============================================================================
|
||||
// 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 });
|
||||
// =============================================================================
|
||||
// Algebraic enums for optionals and results
|
||||
// =============================================================================
|
||||
enum AlgOption {
|
||||
Some(JwtAlg),
|
||||
None,
|
||||
}
|
||||
return CmdOption_MakeNone();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Key type generation
|
||||
// =============================================================================
|
||||
enum KeyType {
|
||||
Rsa,
|
||||
Ecdsa,
|
||||
Ed25519,
|
||||
}
|
||||
func AlgOption_MakeSome(value: JwtAlg) -> AlgOption {
|
||||
let o: AlgOption = AlgOption { tag: AlgOption_Some };
|
||||
o.data.Some_0 = value;
|
||||
return o;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
func AlgOption_MakeNone() -> AlgOption {
|
||||
return AlgOption { tag: AlgOption_None };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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",
|
||||
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();
|
||||
}
|
||||
|
||||
func IsHmac(self: JwtAlg) -> bool {
|
||||
return self.tag == JwtAlg_HS256 || self.tag == JwtAlg_HS384 || self.tag == JwtAlg_HS512;
|
||||
// =============================================================================
|
||||
// Key type generation
|
||||
// =============================================================================
|
||||
enum KeyType {
|
||||
Rsa,
|
||||
Ecdsa,
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
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 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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(f"║ {AppName} — JWT CLI Tool v{Version} ║");
|
||||
PrintLine("║ Sign, verify, decode JSON Web Tokens ║");
|
||||
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||
PrintLine("");
|
||||
PrintLine("Usage:");
|
||||
PrintLine(f" {AppName} sign <alg> <key> <claims>");
|
||||
PrintLine(f" {AppName} verify <token> <alg> <key>");
|
||||
PrintLine(f" {AppName} decode <token>");
|
||||
PrintLine(f" {AppName} keygen <type>");
|
||||
PrintLine("");
|
||||
PrintLine("Commands:");
|
||||
PrintLine(" sign Create a signed JWT from claims JSON");
|
||||
PrintLine(" verify Verify a JWT signature and print payload");
|
||||
PrintLine(" decode Decode a JWT without verification");
|
||||
PrintLine(" keygen Generate cryptographic keys");
|
||||
PrintLine("");
|
||||
PrintLine("Algorithms:");
|
||||
PrintLine(" HS256, HS384, HS512 — HMAC (symmetric)");
|
||||
PrintLine(" RS256, RS384, RS512 — RSA PKCS#1 v1.5");
|
||||
PrintLine(" ES256, ES384 — ECDSA P-256 / P-384");
|
||||
PrintLine(" EdDSA — Ed25519");
|
||||
PrintLine("");
|
||||
PrintLine("Key formats:");
|
||||
PrintLine(" HMAC: raw secret string");
|
||||
PrintLine(" RSA: path to PEM private/public key file");
|
||||
PrintLine(" ECDSA: path to PEM private/public key file");
|
||||
PrintLine(" EdDSA: base64-encoded 32-byte raw key");
|
||||
PrintLine("");
|
||||
PrintLine("Key generation:");
|
||||
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:");
|
||||
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");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Key resolution — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through
|
||||
// =============================================================================
|
||||
func ResolveKey(alg: JwtAlg, keyArg: String) -> KeyResult {
|
||||
if !alg.NeedsPemFile() {
|
||||
return KeyResult_MakeOk(keyArg);
|
||||
}
|
||||
|
||||
if bux_file_exists(keyArg) == 0 {
|
||||
return KeyResult_MakeErr(f"ERROR: PEM file not found: {keyArg}");
|
||||
}
|
||||
|
||||
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) -> 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 {
|
||||
return ExitResult_MakeErr("ERROR: signing failed");
|
||||
}
|
||||
|
||||
PrintLine(token);
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Command: verify
|
||||
// =============================================================================
|
||||
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 = "";
|
||||
|
||||
if !Jwt_Decode(token, alg, key, &header, &payload) {
|
||||
return ExitResult_MakeErr("✗ Signature INVALID (or malformed token)");
|
||||
}
|
||||
|
||||
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) -> ExitResult {
|
||||
let partCount: uint = String_SplitCount(token, ".");
|
||||
if partCount != 3 {
|
||||
return ExitResult_MakeErr("ERROR: not a valid JWT (expected 3 parts)");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
PrintLine("Decoded (no verification):");
|
||||
PrintLine("");
|
||||
PrintLine("Header:");
|
||||
PrintLine(headerJson);
|
||||
PrintLine("");
|
||||
PrintLine("Payload:");
|
||||
PrintLine(payloadJson);
|
||||
PrintLine("");
|
||||
Print("Signature (base64url): ");
|
||||
PrintLine(sigB64);
|
||||
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Command: keygen
|
||||
// =============================================================================
|
||||
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) {
|
||||
return ExitResult_MakeErr("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)");
|
||||
// =============================================================================
|
||||
// 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",
|
||||
}
|
||||
}
|
||||
let pubB64: String = bux_base64_encode(pubBuf as String, 32);
|
||||
let privB64: String = bux_base64_encode(priv as String, 32);
|
||||
PrintLine("Ed25519 keypair (base64):");
|
||||
PrintLine(f" Public: {pubB64}");
|
||||
PrintLine(f" Private: {privB64}");
|
||||
return ExitResult_MakeOk(0);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
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");
|
||||
|
||||
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(f"║ {AppName} — JWT CLI Tool v{Version} ║");
|
||||
PrintLine("║ Sign, verify, decode JSON Web Tokens ║");
|
||||
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||
PrintLine("");
|
||||
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
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("Usage:");
|
||||
PrintLine(f" {AppName} sign <alg> <key> <claims>");
|
||||
PrintLine(f" {AppName} verify <token> <alg> <key>");
|
||||
PrintLine(f" {AppName} decode <token>");
|
||||
PrintLine(f" {AppName} keygen <type>");
|
||||
PrintLine("");
|
||||
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||
PrintLine("Commands:");
|
||||
PrintLine(" sign Create a signed JWT from claims JSON");
|
||||
PrintLine(" verify Verify a JWT signature and print payload");
|
||||
PrintLine(" decode Decode a JWT without verification");
|
||||
PrintLine(" keygen Generate cryptographic keys");
|
||||
PrintLine("");
|
||||
PrintLine("Algorithms:");
|
||||
PrintLine(" HS256, HS384, HS512 — HMAC (symmetric)");
|
||||
PrintLine(" RS256, RS384, RS512 — RSA PKCS#1 v1.5");
|
||||
PrintLine(" ES256, ES384 — ECDSA P-256 / P-384");
|
||||
PrintLine(" EdDSA — Ed25519");
|
||||
PrintLine("");
|
||||
PrintLine("Key formats:");
|
||||
PrintLine(" HMAC: raw secret string");
|
||||
PrintLine(" RSA: path to PEM private/public key file");
|
||||
PrintLine(" ECDSA: path to PEM private/public key file");
|
||||
PrintLine(" EdDSA: base64-encoded 32-byte raw key");
|
||||
PrintLine("");
|
||||
PrintLine("Key generation:");
|
||||
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:");
|
||||
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");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Key resolution — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through
|
||||
// =============================================================================
|
||||
func ResolveKey(alg: JwtAlg, keyArg: String) -> KeyResult {
|
||||
if !alg.NeedsPemFile() {
|
||||
return KeyResult_MakeOk(keyArg);
|
||||
}
|
||||
|
||||
if bux_file_exists(keyArg) == 0 {
|
||||
return KeyResult_MakeErr(f"ERROR: PEM file not found: {keyArg}");
|
||||
}
|
||||
|
||||
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) -> 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 {
|
||||
return ExitResult_MakeErr("ERROR: signing failed");
|
||||
}
|
||||
|
||||
PrintLine(token);
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dispatch a parsed command to its handler
|
||||
// =============================================================================
|
||||
func RunCommand(cmd: Cmd, args: Array<String>) -> ExitResult {
|
||||
if cmd.tag == Cmd_Help {
|
||||
PrintUsage();
|
||||
// =============================================================================
|
||||
// Command: verify
|
||||
// =============================================================================
|
||||
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 = "";
|
||||
|
||||
if !Jwt_Decode(token, alg, key, &header, &payload) {
|
||||
return ExitResult_MakeErr("✗ Signature INVALID (or malformed token)");
|
||||
}
|
||||
|
||||
PrintLine("✓ Signature valid");
|
||||
PrintLine("");
|
||||
PrintLine("Header:");
|
||||
PrintLine(header);
|
||||
PrintLine("");
|
||||
PrintLine("Payload:");
|
||||
PrintLine(payload);
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
if cmd.tag == Cmd_Sign {
|
||||
if args.len < 5 {
|
||||
return ExitResult_MakeErr("ERROR: 'sign' requires: <alg> <key> <claims_json>");
|
||||
|
||||
// =============================================================================
|
||||
// Command: decode (no verification)
|
||||
// =============================================================================
|
||||
func CmdDecode(token: String) -> ExitResult {
|
||||
let partCount: uint = String_SplitCount(token, ".");
|
||||
if partCount != 3 {
|
||||
return ExitResult_MakeErr("ERROR: not a valid JWT (expected 3 parts)");
|
||||
}
|
||||
return CmdSign(args[2], args[3], args[4]);
|
||||
|
||||
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);
|
||||
|
||||
PrintLine("Decoded (no verification):");
|
||||
PrintLine("");
|
||||
PrintLine("Header:");
|
||||
PrintLine(headerJson);
|
||||
PrintLine("");
|
||||
PrintLine("Payload:");
|
||||
PrintLine(payloadJson);
|
||||
PrintLine("");
|
||||
Print("Signature (base64url): ");
|
||||
PrintLine(sigB64);
|
||||
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
if cmd.tag == Cmd_Verify {
|
||||
if args.len < 5 {
|
||||
return ExitResult_MakeErr("ERROR: 'verify' requires: <token> <alg> <key>");
|
||||
|
||||
// =============================================================================
|
||||
// Command: keygen
|
||||
// =============================================================================
|
||||
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");
|
||||
}
|
||||
return CmdVerify(args[2], args[3], args[4]);
|
||||
}
|
||||
if cmd.tag == Cmd_Decode {
|
||||
if args.len < 3 {
|
||||
return ExitResult_MakeErr("ERROR: 'decode' requires: <token>");
|
||||
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) {
|
||||
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):");
|
||||
PrintLine(f" Public: {pubB64}");
|
||||
PrintLine(f" Private: {privB64}");
|
||||
return ExitResult_MakeOk(0);
|
||||
}
|
||||
return CmdDecode(args[2]);
|
||||
}
|
||||
if cmd.tag == Cmd_Keygen {
|
||||
if args.len < 3 {
|
||||
return ExitResult_MakeErr("ERROR: 'keygen' requires: <type>");
|
||||
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 ExitResult_MakeOk(0);
|
||||
}
|
||||
return CmdKeygen(args[2]);
|
||||
}
|
||||
return ExitResult_MakeErr("ERROR: unhandled command");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Entry Point
|
||||
// =============================================================================
|
||||
func Main() -> int {
|
||||
let argc: int = bux_argc();
|
||||
|
||||
// Collect CLI arguments into a generic Array<String>
|
||||
let args: Array<String> = Array_New<String>(argc as uint);
|
||||
for i in 0..argc {
|
||||
Array_Push<String>(&args, bux_argv(i));
|
||||
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 ExitResult_MakeOk(0);
|
||||
}
|
||||
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'");
|
||||
}
|
||||
|
||||
if args.len < 2 {
|
||||
PrintUsage();
|
||||
return 0;
|
||||
// =============================================================================
|
||||
// Dispatch a parsed command to its handler
|
||||
// =============================================================================
|
||||
func RunCommand(cmd: Cmd, args: Array<String>) -> 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: <alg> <key> <claims_json>");
|
||||
}
|
||||
return CmdSign(args[2], args[3], args[4]);
|
||||
}
|
||||
if cmd.tag == Cmd_Verify {
|
||||
if args.len < 5 {
|
||||
return ExitResult_MakeErr("ERROR: 'verify' requires: <token> <alg> <key>");
|
||||
}
|
||||
return CmdVerify(args[2], args[3], args[4]);
|
||||
}
|
||||
if cmd.tag == Cmd_Decode {
|
||||
if args.len < 3 {
|
||||
return ExitResult_MakeErr("ERROR: 'decode' requires: <token>");
|
||||
}
|
||||
return CmdDecode(args[2]);
|
||||
}
|
||||
if cmd.tag == Cmd_Keygen {
|
||||
if args.len < 3 {
|
||||
return ExitResult_MakeErr("ERROR: 'keygen' requires: <type>");
|
||||
}
|
||||
return CmdKeygen(args[2]);
|
||||
}
|
||||
return ExitResult_MakeErr("ERROR: unhandled command");
|
||||
}
|
||||
|
||||
let command: String = args[1];
|
||||
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;
|
||||
// =============================================================================
|
||||
// Main Entry Point
|
||||
// =============================================================================
|
||||
func Main() -> int {
|
||||
let argc: int = bux_argc();
|
||||
|
||||
let result: ExitResult = RunCommand(cmd, args);
|
||||
if result.tag == ExitResult_Err {
|
||||
PrintLine(result.data.Err_0);
|
||||
return 1;
|
||||
// Collect CLI arguments into a generic Array<String>
|
||||
let args: Array<String> = Array_New<String>(argc as uint);
|
||||
for i in 0..argc {
|
||||
Array_Push<String>(&args, bux_argv(i));
|
||||
}
|
||||
|
||||
if args.len < 2 {
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
let command: String = args[1];
|
||||
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;
|
||||
|
||||
let result: ExitResult = RunCommand(cmd, args);
|
||||
if result.tag == ExitResult_Err {
|
||||
PrintLine(result.data.Err_0);
|
||||
return 1;
|
||||
}
|
||||
return result.data.Ok_0;
|
||||
}
|
||||
return result.data.Ok_0;
|
||||
}
|
||||
|
||||
} // module Main
|
||||
|
||||
+16
-16
@@ -1,21 +1,21 @@
|
||||
module Config {
|
||||
|
||||
pub struct ServerConfig {
|
||||
bindAddr: String;
|
||||
port: int;
|
||||
workerCount: int;
|
||||
publicDir: String;
|
||||
backlog: int;
|
||||
}
|
||||
pub struct ServerConfig {
|
||||
bindAddr: String;
|
||||
port: int;
|
||||
workerCount: int;
|
||||
publicDir: String;
|
||||
backlog: int;
|
||||
}
|
||||
|
||||
pub const func DefaultConfig() -> ServerConfig {
|
||||
return ServerConfig {
|
||||
bindAddr: "0.0.0.0",
|
||||
port: 8080,
|
||||
workerCount: 4,
|
||||
publicDir: "public",
|
||||
backlog: 128,
|
||||
};
|
||||
}
|
||||
pub const func DefaultConfig() -> ServerConfig {
|
||||
return ServerConfig {
|
||||
bindAddr: "0.0.0.0",
|
||||
port: 8080,
|
||||
workerCount: 4,
|
||||
publicDir: "public",
|
||||
backlog: 128,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+50
-50
@@ -1,65 +1,65 @@
|
||||
module Errors {
|
||||
|
||||
import Http::{HttpRequest};
|
||||
import Http::{HttpRequest};
|
||||
|
||||
pub enum HttpError {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
MethodNotAllowed,
|
||||
InternalError(String),
|
||||
}
|
||||
pub enum HttpError {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
MethodNotAllowed,
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
pub enum ParseResult {
|
||||
Ok(HttpRequest),
|
||||
Err(HttpError),
|
||||
}
|
||||
pub enum ParseResult {
|
||||
Ok(HttpRequest),
|
||||
Err(HttpError),
|
||||
}
|
||||
|
||||
pub enum FileResult {
|
||||
Ok(String),
|
||||
Err(HttpError),
|
||||
}
|
||||
pub enum FileResult {
|
||||
Ok(String),
|
||||
Err(HttpError),
|
||||
}
|
||||
|
||||
pub func ParseResult_NewOk(req: HttpRequest) -> ParseResult {
|
||||
let r: ParseResult = ParseResult { tag: ParseResult_Ok };
|
||||
r.data.Ok_0 = req;
|
||||
return r;
|
||||
}
|
||||
pub func ParseResult_NewOk(req: HttpRequest) -> ParseResult {
|
||||
let r: ParseResult = ParseResult { tag: ParseResult_Ok };
|
||||
r.data.Ok_0 = req;
|
||||
return r;
|
||||
}
|
||||
|
||||
pub func ParseResult_NewErr(err: HttpError) -> ParseResult {
|
||||
let r: ParseResult = ParseResult { tag: ParseResult_Err };
|
||||
r.data.Err_0 = err;
|
||||
return r;
|
||||
}
|
||||
pub func ParseResult_NewErr(err: HttpError) -> ParseResult {
|
||||
let r: ParseResult = ParseResult { tag: ParseResult_Err };
|
||||
r.data.Err_0 = err;
|
||||
return r;
|
||||
}
|
||||
|
||||
pub func FileResult_NewOk(content: String) -> FileResult {
|
||||
let r: FileResult = FileResult { tag: FileResult_Ok };
|
||||
r.data.Ok_0 = content;
|
||||
return r;
|
||||
}
|
||||
pub func FileResult_NewOk(content: String) -> FileResult {
|
||||
let r: FileResult = FileResult { tag: FileResult_Ok };
|
||||
r.data.Ok_0 = content;
|
||||
return r;
|
||||
}
|
||||
|
||||
pub func FileResult_NewErr(err: HttpError) -> FileResult {
|
||||
let r: FileResult = FileResult { tag: FileResult_Err };
|
||||
r.data.Err_0 = err;
|
||||
return r;
|
||||
}
|
||||
pub func FileResult_NewErr(err: HttpError) -> FileResult {
|
||||
let r: FileResult = FileResult { tag: FileResult_Err };
|
||||
r.data.Err_0 = err;
|
||||
return r;
|
||||
}
|
||||
|
||||
pub func FileResult_IsOk(r: FileResult) -> bool {
|
||||
return r.tag == FileResult_Ok;
|
||||
}
|
||||
pub func FileResult_IsOk(r: FileResult) -> bool {
|
||||
return r.tag == FileResult_Ok;
|
||||
}
|
||||
|
||||
pub func FileResult_Unwrap(r: FileResult) -> String {
|
||||
return r.data.Ok_0;
|
||||
}
|
||||
pub func FileResult_Unwrap(r: FileResult) -> String {
|
||||
return r.data.Ok_0;
|
||||
}
|
||||
|
||||
pub func FileResult_UnwrapErr(r: FileResult) -> HttpError {
|
||||
return r.data.Err_0;
|
||||
}
|
||||
pub func FileResult_UnwrapErr(r: FileResult) -> HttpError {
|
||||
return r.data.Err_0;
|
||||
}
|
||||
|
||||
pub func HttpError_ToString(err: HttpError) -> String {
|
||||
if err.tag == HttpError_BadRequest { return "Bad Request"; }
|
||||
if err.tag == HttpError_NotFound { return "Not Found"; }
|
||||
if err.tag == HttpError_MethodNotAllowed { return "Method Not Allowed"; }
|
||||
return err.data.InternalError_0;
|
||||
}
|
||||
pub func HttpError_ToString(err: HttpError) -> String {
|
||||
if err.tag == HttpError_BadRequest { return "Bad Request"; }
|
||||
if err.tag == HttpError_NotFound { return "Not Found"; }
|
||||
if err.tag == HttpError_MethodNotAllowed { return "Method Not Allowed"; }
|
||||
return err.data.InternalError_0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+72
-72
@@ -1,94 +1,94 @@
|
||||
module Handlers {
|
||||
|
||||
import Std::String::{String_Eq, String_Contains};
|
||||
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse, Http_MimeType, RequestHeader_Get};
|
||||
import Errors::{HttpError, FileResult, FileResult_NewOk, FileResult_NewErr, FileResult_IsOk, FileResult_Unwrap, FileResult_UnwrapErr, HttpError_ToString};
|
||||
import Std::String::{String_Eq, String_Contains};
|
||||
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse, Http_MimeType, RequestHeader_Get};
|
||||
import Errors::{HttpError, FileResult, FileResult_NewOk, FileResult_NewErr, FileResult_IsOk, FileResult_Unwrap, FileResult_UnwrapErr, HttpError_ToString};
|
||||
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
extern func bux_file_exists(path: String) -> int;
|
||||
extern func bux_read_file(path: String) -> String;
|
||||
extern func bux_path_join(a: String, b: String) -> 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_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
extern func bux_file_exists(path: String) -> int;
|
||||
extern func bux_read_file(path: String) -> String;
|
||||
extern func bux_path_join(a: String, b: String) -> 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_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
|
||||
pub func NotFoundResponse() -> HttpResponse {
|
||||
return Http_NewResponse(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||
}
|
||||
|
||||
pub func MethodNotAllowedResponse() -> HttpResponse {
|
||||
return Http_NewResponse(405, "text/plain; charset=utf-8", "Method Not Allowed");
|
||||
}
|
||||
|
||||
pub func ReadStaticFile(requestPath: String) -> FileResult {
|
||||
if String_Contains(requestPath, "..") {
|
||||
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||
pub func NotFoundResponse() -> HttpResponse {
|
||||
return Http_NewResponse(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||
}
|
||||
|
||||
var filePath: String = requestPath;
|
||||
if String_Eq(filePath, "/") {
|
||||
filePath = "/index.html";
|
||||
pub func MethodNotAllowedResponse() -> HttpResponse {
|
||||
return Http_NewResponse(405, "text/plain; charset=utf-8", "Method Not Allowed");
|
||||
}
|
||||
|
||||
let fullPath: String = bux_path_join("public", filePath);
|
||||
if bux_file_exists(fullPath) == 0 {
|
||||
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||
pub func ReadStaticFile(requestPath: String) -> FileResult {
|
||||
if String_Contains(requestPath, "..") {
|
||||
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||
}
|
||||
|
||||
var filePath: String = requestPath;
|
||||
if String_Eq(filePath, "/") {
|
||||
filePath = "/index.html";
|
||||
}
|
||||
|
||||
let fullPath: String = bux_path_join("public", filePath);
|
||||
if bux_file_exists(fullPath) == 0 {
|
||||
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||
}
|
||||
|
||||
let content: String = bux_read_file(fullPath);
|
||||
return FileResult_NewOk(content);
|
||||
}
|
||||
|
||||
let content: String = bux_read_file(fullPath);
|
||||
return FileResult_NewOk(content);
|
||||
}
|
||||
|
||||
func FileErrorResponse(err: HttpError) -> HttpResponse {
|
||||
match err {
|
||||
HttpError::NotFound => NotFoundResponse(),
|
||||
_ => Http_NewResponse(500, "text/plain; charset=utf-8", HttpError_ToString(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub func ServeStaticFile(req: HttpRequest) -> HttpResponse {
|
||||
if req.method != HttpMethod_GET && req.method != HttpMethod_HEAD {
|
||||
return MethodNotAllowedResponse();
|
||||
func FileErrorResponse(err: HttpError) -> HttpResponse {
|
||||
match err {
|
||||
HttpError::NotFound => NotFoundResponse(),
|
||||
_ => Http_NewResponse(500, "text/plain; charset=utf-8", HttpError_ToString(err)),
|
||||
}
|
||||
}
|
||||
|
||||
let result: FileResult = ReadStaticFile(req.path);
|
||||
if FileResult_IsOk(result) {
|
||||
let content: String = FileResult_Unwrap(result);
|
||||
return Http_NewResponse(200, Http_MimeType(req.path), content);
|
||||
}
|
||||
return FileErrorResponse(FileResult_UnwrapErr(result));
|
||||
}
|
||||
pub func ServeStaticFile(req: HttpRequest) -> HttpResponse {
|
||||
if req.method != HttpMethod_GET && req.method != HttpMethod_HEAD {
|
||||
return MethodNotAllowedResponse();
|
||||
}
|
||||
|
||||
pub func HandleApiHealth() -> HttpResponse {
|
||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||
let result: FileResult = ReadStaticFile(req.path);
|
||||
if FileResult_IsOk(result) {
|
||||
let content: String = FileResult_Unwrap(result);
|
||||
return Http_NewResponse(200, Http_MimeType(req.path), content);
|
||||
}
|
||||
return FileErrorResponse(FileResult_UnwrapErr(result));
|
||||
}
|
||||
|
||||
pub func HandleApiHealth() -> HttpResponse {
|
||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||
"{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.2.0\"}");
|
||||
}
|
||||
}
|
||||
|
||||
pub func HandleApiInfo() -> HttpResponse {
|
||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||
pub func HandleApiInfo() -> HttpResponse {
|
||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||
"{\"name\":\"Nexus\",\"language\":\"Bux\",\"features\":[\"HTTP/1.1\",\"thread-pool\",\"algebraic-enums\"]}");
|
||||
}
|
||||
}
|
||||
|
||||
pub func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse {
|
||||
let wsKey: String = RequestHeader_Get(req, "Sec-WebSocket-Key");
|
||||
pub func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse {
|
||||
let wsKey: String = RequestHeader_Get(req, "Sec-WebSocket-Key");
|
||||
|
||||
var resp: HttpResponse;
|
||||
resp.statusCode = 101;
|
||||
resp.contentType = "";
|
||||
resp.body = "";
|
||||
var resp: HttpResponse;
|
||||
resp.statusCode = 101;
|
||||
resp.contentType = "";
|
||||
resp.body = "";
|
||||
|
||||
let sb: *void = bux_sb_new(256);
|
||||
bux_sb_append(sb, "Upgrade: websocket\r\n");
|
||||
bux_sb_append(sb, "Connection: Upgrade\r\n");
|
||||
bux_sb_append(sb, "Sec-WebSocket-Accept: ");
|
||||
bux_sb_append(sb, wsKey);
|
||||
bux_sb_append(sb, "\r\n");
|
||||
resp.extraHeaders = bux_sb_build(sb);
|
||||
bux_sb_free(sb);
|
||||
let sb: *void = bux_sb_new(256);
|
||||
bux_sb_append(sb, "Upgrade: websocket\r\n");
|
||||
bux_sb_append(sb, "Connection: Upgrade\r\n");
|
||||
bux_sb_append(sb, "Sec-WebSocket-Accept: ");
|
||||
bux_sb_append(sb, wsKey);
|
||||
bux_sb_append(sb, "\r\n");
|
||||
resp.extraHeaders = bux_sb_build(sb);
|
||||
bux_sb_free(sb);
|
||||
|
||||
return resp;
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+97
-97
@@ -1,107 +1,107 @@
|
||||
module Http {
|
||||
|
||||
import Std::Array::{Array};
|
||||
import Std::String::{String_Eq, String_EndsWith, String_Contains};
|
||||
import Std::Array::{Array};
|
||||
import Std::String::{String_Eq, String_EndsWith, String_Contains};
|
||||
|
||||
pub enum HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
pub struct HeaderEntry {
|
||||
key: String;
|
||||
value: String;
|
||||
}
|
||||
|
||||
pub struct HttpRequest {
|
||||
method: HttpMethod;
|
||||
path: String;
|
||||
version: String;
|
||||
body: String;
|
||||
headers: Array<HeaderEntry>;
|
||||
}
|
||||
|
||||
pub struct HttpResponse {
|
||||
statusCode: int;
|
||||
contentType: String;
|
||||
body: String;
|
||||
extraHeaders: String;
|
||||
}
|
||||
|
||||
pub func Http_StatusText(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 == 304 { return "Not Modified"; }
|
||||
if code == 400 { return "Bad Request"; }
|
||||
if code == 401 { return "Unauthorized"; }
|
||||
if code == 403 { return "Forbidden"; }
|
||||
if code == 404 { return "Not Found"; }
|
||||
if code == 405 { return "Method Not Allowed"; }
|
||||
if code == 413 { return "Payload Too Large"; }
|
||||
if code == 414 { return "URI Too Long"; }
|
||||
if code == 500 { return "Internal Server Error"; }
|
||||
if code == 501 { return "Not Implemented"; }
|
||||
if code == 503 { return "Service Unavailable"; }
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
pub func Http_MimeType(path: String) -> String {
|
||||
if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".png") { return "image/png"; }
|
||||
if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; }
|
||||
if String_EndsWith(path, ".gif") { return "image/gif"; }
|
||||
if String_EndsWith(path, ".svg") { return "image/svg+xml"; }
|
||||
if String_EndsWith(path, ".ico") { return "image/x-icon"; }
|
||||
if String_EndsWith(path, ".webp") { return "image/webp"; }
|
||||
if String_EndsWith(path, ".woff2") { return "font/woff2"; }
|
||||
if String_EndsWith(path, ".woff") { return "font/woff"; }
|
||||
if String_EndsWith(path, ".wasm") { return "application/wasm"; }
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
pub func Http_MethodName(m: HttpMethod) -> String {
|
||||
match m {
|
||||
HttpMethod::GET => "GET",
|
||||
HttpMethod::POST => "POST",
|
||||
HttpMethod::PUT => "PUT",
|
||||
HttpMethod::DELETE => "DELETE",
|
||||
HttpMethod::PATCH => "PATCH",
|
||||
HttpMethod::HEAD => "HEAD",
|
||||
HttpMethod::OPTIONS => "OPTIONS",
|
||||
HttpMethod::UNKNOWN => "UNKNOWN",
|
||||
pub enum HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
pub func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse {
|
||||
var resp: HttpResponse;
|
||||
resp.statusCode = code;
|
||||
resp.contentType = contentType;
|
||||
resp.body = body;
|
||||
resp.extraHeaders = "";
|
||||
return resp;
|
||||
}
|
||||
pub struct HeaderEntry {
|
||||
key: String;
|
||||
value: String;
|
||||
}
|
||||
|
||||
pub func RequestHeader_Get(req: HttpRequest, key: String) -> String {
|
||||
for entry in req.headers {
|
||||
if String_Eq(entry.key, key) {
|
||||
return entry.value;
|
||||
pub struct HttpRequest {
|
||||
method: HttpMethod;
|
||||
path: String;
|
||||
version: String;
|
||||
body: String;
|
||||
headers: Array<HeaderEntry>;
|
||||
}
|
||||
|
||||
pub struct HttpResponse {
|
||||
statusCode: int;
|
||||
contentType: String;
|
||||
body: String;
|
||||
extraHeaders: String;
|
||||
}
|
||||
|
||||
pub func Http_StatusText(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 == 304 { return "Not Modified"; }
|
||||
if code == 400 { return "Bad Request"; }
|
||||
if code == 401 { return "Unauthorized"; }
|
||||
if code == 403 { return "Forbidden"; }
|
||||
if code == 404 { return "Not Found"; }
|
||||
if code == 405 { return "Method Not Allowed"; }
|
||||
if code == 413 { return "Payload Too Large"; }
|
||||
if code == 414 { return "URI Too Long"; }
|
||||
if code == 500 { return "Internal Server Error"; }
|
||||
if code == 501 { return "Not Implemented"; }
|
||||
if code == 503 { return "Service Unavailable"; }
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
pub func Http_MimeType(path: String) -> String {
|
||||
if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".png") { return "image/png"; }
|
||||
if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; }
|
||||
if String_EndsWith(path, ".gif") { return "image/gif"; }
|
||||
if String_EndsWith(path, ".svg") { return "image/svg+xml"; }
|
||||
if String_EndsWith(path, ".ico") { return "image/x-icon"; }
|
||||
if String_EndsWith(path, ".webp") { return "image/webp"; }
|
||||
if String_EndsWith(path, ".woff2") { return "font/woff2"; }
|
||||
if String_EndsWith(path, ".woff") { return "font/woff"; }
|
||||
if String_EndsWith(path, ".wasm") { return "application/wasm"; }
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
pub func Http_MethodName(m: HttpMethod) -> String {
|
||||
match m {
|
||||
HttpMethod::GET => "GET",
|
||||
HttpMethod::POST => "POST",
|
||||
HttpMethod::PUT => "PUT",
|
||||
HttpMethod::DELETE => "DELETE",
|
||||
HttpMethod::PATCH => "PATCH",
|
||||
HttpMethod::HEAD => "HEAD",
|
||||
HttpMethod::OPTIONS => "OPTIONS",
|
||||
HttpMethod::UNKNOWN => "UNKNOWN",
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
pub func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse {
|
||||
var resp: HttpResponse;
|
||||
resp.statusCode = code;
|
||||
resp.contentType = contentType;
|
||||
resp.body = body;
|
||||
resp.extraHeaders = "";
|
||||
return resp;
|
||||
}
|
||||
|
||||
pub func RequestHeader_Get(req: HttpRequest, key: String) -> String {
|
||||
for entry in req.headers {
|
||||
if String_Eq(entry.key, key) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+37
-37
@@ -1,49 +1,49 @@
|
||||
module Main {
|
||||
|
||||
import Config::{ServerConfig, DefaultConfig};
|
||||
import Http::{HttpMethod};
|
||||
import Router::{Handler, Route, Router};
|
||||
import Server::{RunServer};
|
||||
import Std::Array::{Array, Array_New, Array_Push};
|
||||
import Config::{ServerConfig, DefaultConfig};
|
||||
import Http::{HttpMethod};
|
||||
import Router::{Handler, Route, Router};
|
||||
import Server::{RunServer};
|
||||
import Std::Array::{Array, Array_New, Array_Push};
|
||||
|
||||
func BuildRouter() -> Router {
|
||||
var routes: Array<Route> = Array_New<Route>(8);
|
||||
func BuildRouter() -> Router {
|
||||
var routes: Array<Route> = Array_New<Route>(8);
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/api/health",
|
||||
handler: Handler { tag: Handler_ApiHealth },
|
||||
});
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/api/health",
|
||||
handler: Handler { tag: Handler_ApiHealth },
|
||||
});
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/api/info",
|
||||
handler: Handler { tag: Handler_ApiInfo },
|
||||
});
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/api/info",
|
||||
handler: Handler { tag: Handler_ApiInfo },
|
||||
});
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/ws",
|
||||
handler: Handler { tag: Handler_WsUpgrade },
|
||||
});
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/ws",
|
||||
handler: Handler { tag: Handler_WsUpgrade },
|
||||
});
|
||||
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/",
|
||||
handler: Handler { tag: Handler_StaticFile },
|
||||
});
|
||||
Array_Push<Route>(&routes, Route {
|
||||
method: HttpMethod { tag: HttpMethod_GET },
|
||||
path: "/",
|
||||
handler: Handler { tag: Handler_StaticFile },
|
||||
});
|
||||
|
||||
return Router {
|
||||
routes: routes,
|
||||
notFound: Handler { tag: Handler_NotFound },
|
||||
};
|
||||
}
|
||||
return Router {
|
||||
routes: routes,
|
||||
notFound: Handler { tag: Handler_NotFound },
|
||||
};
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let config: ServerConfig = DefaultConfig();
|
||||
let router: Router = BuildRouter();
|
||||
func Main() -> int {
|
||||
let config: ServerConfig = DefaultConfig();
|
||||
let router: Router = BuildRouter();
|
||||
|
||||
return RunServer(config, router);
|
||||
}
|
||||
return RunServer(config, router);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+110
-110
@@ -1,130 +1,130 @@
|
||||
module Parser {
|
||||
|
||||
import Std::String::{String_Len, String_Eq, String_Trim};
|
||||
import Std::Array::{Array, Array_New, Array_Push};
|
||||
import Http::{HttpMethod, HttpRequest, HeaderEntry};
|
||||
import Errors::{HttpError, ParseResult, ParseResult_NewOk, ParseResult_NewErr};
|
||||
import Std::String::{String_Len, String_Eq, String_Trim};
|
||||
import Std::Array::{Array, Array_New, Array_Push};
|
||||
import Http::{HttpMethod, HttpRequest, HeaderEntry};
|
||||
import Errors::{HttpError, ParseResult, ParseResult_NewOk, ParseResult_NewErr};
|
||||
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
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_offset(pos: String, base: String) -> uint;
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
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_offset(pos: String, base: String) -> uint;
|
||||
|
||||
pub func ParseMethod(s: String) -> HttpMethod {
|
||||
if String_Eq(s, "GET") { return HttpMethod { tag: HttpMethod_GET }; }
|
||||
if String_Eq(s, "POST") { return HttpMethod { tag: HttpMethod_POST }; }
|
||||
if String_Eq(s, "PUT") { return HttpMethod { tag: HttpMethod_PUT }; }
|
||||
if String_Eq(s, "DELETE") { return HttpMethod { tag: HttpMethod_DELETE }; }
|
||||
if String_Eq(s, "PATCH") { return HttpMethod { tag: HttpMethod_PATCH }; }
|
||||
if String_Eq(s, "HEAD") { return HttpMethod { tag: HttpMethod_HEAD }; }
|
||||
if String_Eq(s, "OPTIONS") { return HttpMethod { tag: HttpMethod_OPTIONS }; }
|
||||
return HttpMethod { tag: HttpMethod_UNKNOWN };
|
||||
}
|
||||
pub func ParseMethod(s: String) -> HttpMethod {
|
||||
if String_Eq(s, "GET") { return HttpMethod { tag: HttpMethod_GET }; }
|
||||
if String_Eq(s, "POST") { return HttpMethod { tag: HttpMethod_POST }; }
|
||||
if String_Eq(s, "PUT") { return HttpMethod { tag: HttpMethod_PUT }; }
|
||||
if String_Eq(s, "DELETE") { return HttpMethod { tag: HttpMethod_DELETE }; }
|
||||
if String_Eq(s, "PATCH") { return HttpMethod { tag: HttpMethod_PATCH }; }
|
||||
if String_Eq(s, "HEAD") { return HttpMethod { tag: HttpMethod_HEAD }; }
|
||||
if String_Eq(s, "OPTIONS") { return HttpMethod { tag: HttpMethod_OPTIONS }; }
|
||||
return HttpMethod { tag: HttpMethod_UNKNOWN };
|
||||
}
|
||||
|
||||
func Slice(raw: String, start: int, len: int) -> String {
|
||||
if start < 0 || len <= 0 { return ""; }
|
||||
return bux_str_slice(raw, start as uint, len as uint);
|
||||
}
|
||||
func Slice(raw: String, start: int, len: int) -> String {
|
||||
if start < 0 || len <= 0 { return ""; }
|
||||
return bux_str_slice(raw, start as uint, len as uint);
|
||||
}
|
||||
|
||||
func FindCrlf(raw: String, start: int) -> int {
|
||||
let rawLen: uint = bux_strlen(raw);
|
||||
if start as uint >= rawLen { return -1; }
|
||||
let tail: String = bux_str_slice(raw, start as uint, rawLen - start as uint);
|
||||
let hit: String = bux_strstr(tail, "\r\n");
|
||||
if String_Len(hit) == 0 { return -1; }
|
||||
let offset: uint = bux_str_offset(hit, tail);
|
||||
return start + offset as int;
|
||||
}
|
||||
func FindCrlf(raw: String, start: int) -> int {
|
||||
let rawLen: uint = bux_strlen(raw);
|
||||
if start as uint >= rawLen { return -1; }
|
||||
let tail: String = bux_str_slice(raw, start as uint, rawLen - start as uint);
|
||||
let hit: String = bux_strstr(tail, "\r\n");
|
||||
if String_Len(hit) == 0 { return -1; }
|
||||
let offset: uint = bux_str_offset(hit, tail);
|
||||
return start + offset as int;
|
||||
}
|
||||
|
||||
func ParseHeaders(raw: String, start: int, end: int) -> Array<HeaderEntry> {
|
||||
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||
var pos: int = start;
|
||||
while pos < end {
|
||||
let lineEnd: int = FindCrlf(raw, pos);
|
||||
if lineEnd < 0 || lineEnd >= end { break; }
|
||||
let lineLen: int = lineEnd - pos;
|
||||
if lineLen > 0 {
|
||||
let line: String = Slice(raw, pos, lineLen);
|
||||
let colon: String = bux_strstr(line, ":");
|
||||
if String_Len(colon) > 0 {
|
||||
let keyLen: uint = bux_str_offset(colon, line);
|
||||
let key: String = String_Trim(Slice(line, 0, keyLen as int));
|
||||
let valStart: int = keyLen as int + 1;
|
||||
let val: String = String_Trim(Slice(line, valStart, lineLen - valStart));
|
||||
let entry: HeaderEntry = HeaderEntry { key: key, value: val };
|
||||
Array_Push<HeaderEntry>(&headers, entry);
|
||||
func ParseHeaders(raw: String, start: int, end: int) -> Array<HeaderEntry> {
|
||||
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||
var pos: int = start;
|
||||
while pos < end {
|
||||
let lineEnd: int = FindCrlf(raw, pos);
|
||||
if lineEnd < 0 || lineEnd >= end { break; }
|
||||
let lineLen: int = lineEnd - pos;
|
||||
if lineLen > 0 {
|
||||
let line: String = Slice(raw, pos, lineLen);
|
||||
let colon: String = bux_strstr(line, ":");
|
||||
if String_Len(colon) > 0 {
|
||||
let keyLen: uint = bux_str_offset(colon, line);
|
||||
let key: String = String_Trim(Slice(line, 0, keyLen as int));
|
||||
let valStart: int = keyLen as int + 1;
|
||||
let val: String = String_Trim(Slice(line, valStart, lineLen - valStart));
|
||||
let entry: HeaderEntry = HeaderEntry { key: key, value: val };
|
||||
Array_Push<HeaderEntry>(&headers, entry);
|
||||
}
|
||||
}
|
||||
pos = lineEnd + 2;
|
||||
}
|
||||
pos = lineEnd + 2;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
pub func ParseRequest(raw: String) -> ParseResult {
|
||||
let rawLen: uint = bux_strlen(raw);
|
||||
if rawLen == 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Find end of request line
|
||||
let lineEnd: int = FindCrlf(raw, 0);
|
||||
if lineEnd < 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
pub func ParseRequest(raw: String) -> ParseResult {
|
||||
let rawLen: uint = bux_strlen(raw);
|
||||
if rawLen == 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
|
||||
// Split request line: METHOD PATH VERSION
|
||||
var methodEnd: int = -1;
|
||||
var pathStart: int = -1;
|
||||
var pathEnd: int = -1;
|
||||
var i: int = 0;
|
||||
while i < lineEnd {
|
||||
if raw[i] as int == 32 { // space
|
||||
if methodEnd < 0 {
|
||||
methodEnd = i;
|
||||
pathStart = i + 1;
|
||||
} else if pathEnd < 0 {
|
||||
pathEnd = i;
|
||||
break;
|
||||
// Find end of request line
|
||||
let lineEnd: int = FindCrlf(raw, 0);
|
||||
if lineEnd < 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
|
||||
// Split request line: METHOD PATH VERSION
|
||||
var methodEnd: int = -1;
|
||||
var pathStart: int = -1;
|
||||
var pathEnd: int = -1;
|
||||
var i: int = 0;
|
||||
while i < lineEnd {
|
||||
if raw[i] as int == 32 { // space
|
||||
if methodEnd < 0 {
|
||||
methodEnd = i;
|
||||
pathStart = i + 1;
|
||||
} else if pathEnd < 0 {
|
||||
pathEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
if methodEnd < 0 || pathStart < 0 || pathEnd < 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
|
||||
let methodStr: String = Slice(raw, 0, methodEnd);
|
||||
let path: String = Slice(raw, pathStart, pathEnd - pathStart);
|
||||
let version: String = Slice(raw, pathEnd + 1, lineEnd - pathEnd - 1);
|
||||
|
||||
// Find header/body boundary
|
||||
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
||||
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||
var body: String = "";
|
||||
if String_Len(boundary) > 0 {
|
||||
let headerEnd: uint = bux_str_offset(boundary, raw);
|
||||
headers = ParseHeaders(raw, lineEnd + 2, headerEnd as int);
|
||||
let bodyStart: uint = headerEnd + 4;
|
||||
if bodyStart < rawLen {
|
||||
body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
|
||||
if methodEnd < 0 || pathStart < 0 || pathEnd < 0 {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
} else {
|
||||
headers = ParseHeaders(raw, lineEnd + 2, rawLen as int);
|
||||
}
|
||||
|
||||
if String_Eq(path, "") {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
let methodStr: String = Slice(raw, 0, methodEnd);
|
||||
let path: String = Slice(raw, pathStart, pathEnd - pathStart);
|
||||
let version: String = Slice(raw, pathEnd + 1, lineEnd - pathEnd - 1);
|
||||
|
||||
let req: HttpRequest = HttpRequest {
|
||||
method: ParseMethod(methodStr),
|
||||
path: path,
|
||||
version: version,
|
||||
body: body,
|
||||
headers: headers,
|
||||
};
|
||||
return ParseResult_NewOk(req);
|
||||
}
|
||||
// Find header/body boundary
|
||||
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
||||
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||
var body: String = "";
|
||||
if String_Len(boundary) > 0 {
|
||||
let headerEnd: uint = bux_str_offset(boundary, raw);
|
||||
headers = ParseHeaders(raw, lineEnd + 2, headerEnd as int);
|
||||
let bodyStart: uint = headerEnd + 4;
|
||||
if bodyStart < rawLen {
|
||||
body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
|
||||
}
|
||||
} else {
|
||||
headers = ParseHeaders(raw, lineEnd + 2, rawLen as int);
|
||||
}
|
||||
|
||||
if String_Eq(path, "") {
|
||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||
}
|
||||
|
||||
let req: HttpRequest = HttpRequest {
|
||||
method: ParseMethod(methodStr),
|
||||
path: path,
|
||||
version: version,
|
||||
body: body,
|
||||
headers: headers,
|
||||
};
|
||||
return ParseResult_NewOk(req);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+37
-37
@@ -1,46 +1,46 @@
|
||||
module Router {
|
||||
|
||||
import Std::Array::{Array};
|
||||
import Std::String::{String_Eq};
|
||||
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse};
|
||||
import Handlers::{ServeStaticFile, HandleApiHealth, HandleApiInfo, HandleWebSocketUpgrade, NotFoundResponse};
|
||||
import Std::Array::{Array};
|
||||
import Std::String::{String_Eq};
|
||||
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse};
|
||||
import Handlers::{ServeStaticFile, HandleApiHealth, HandleApiInfo, HandleWebSocketUpgrade, NotFoundResponse};
|
||||
|
||||
pub enum Handler {
|
||||
StaticFile,
|
||||
ApiHealth,
|
||||
ApiInfo,
|
||||
WsUpgrade,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
pub struct Route {
|
||||
method: HttpMethod;
|
||||
path: String;
|
||||
handler: Handler;
|
||||
}
|
||||
|
||||
pub struct Router {
|
||||
routes: Array<Route>;
|
||||
notFound: Handler;
|
||||
}
|
||||
|
||||
pub func Handler_Handle(h: Handler, req: HttpRequest) -> HttpResponse {
|
||||
match h {
|
||||
Handler::StaticFile => ServeStaticFile(req),
|
||||
Handler::ApiHealth => HandleApiHealth(),
|
||||
Handler::ApiInfo => HandleApiInfo(),
|
||||
Handler::WsUpgrade => HandleWebSocketUpgrade(req),
|
||||
Handler::NotFound => NotFoundResponse(),
|
||||
pub enum Handler {
|
||||
StaticFile,
|
||||
ApiHealth,
|
||||
ApiInfo,
|
||||
WsUpgrade,
|
||||
NotFound,
|
||||
}
|
||||
}
|
||||
|
||||
pub func Router_Dispatch(r: Router, req: HttpRequest) -> HttpResponse {
|
||||
for route in r.routes {
|
||||
if route.method == req.method && String_Eq(route.path, req.path) {
|
||||
return Handler_Handle(route.handler, req);
|
||||
pub struct Route {
|
||||
method: HttpMethod;
|
||||
path: String;
|
||||
handler: Handler;
|
||||
}
|
||||
|
||||
pub struct Router {
|
||||
routes: Array<Route>;
|
||||
notFound: Handler;
|
||||
}
|
||||
|
||||
pub func Handler_Handle(h: Handler, req: HttpRequest) -> HttpResponse {
|
||||
match h {
|
||||
Handler::StaticFile => ServeStaticFile(req),
|
||||
Handler::ApiHealth => HandleApiHealth(),
|
||||
Handler::ApiInfo => HandleApiInfo(),
|
||||
Handler::WsUpgrade => HandleWebSocketUpgrade(req),
|
||||
Handler::NotFound => NotFoundResponse(),
|
||||
}
|
||||
}
|
||||
return Handler_Handle(r.notFound, req);
|
||||
}
|
||||
|
||||
pub func Router_Dispatch(r: Router, req: HttpRequest) -> HttpResponse {
|
||||
for route in r.routes {
|
||||
if route.method == req.method && String_Eq(route.path, req.path) {
|
||||
return Handler_Handle(route.handler, req);
|
||||
}
|
||||
}
|
||||
return Handler_Handle(r.notFound, req);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+148
-148
@@ -1,176 +1,176 @@
|
||||
module Server {
|
||||
|
||||
import Std::Io::{Print, PrintLine, 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_StartsWith};
|
||||
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
||||
import Config::{ServerConfig};
|
||||
import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse};
|
||||
import Errors::{ParseResult};
|
||||
import Parser::{ParseRequest};
|
||||
import Router::{Router, Router_Dispatch};
|
||||
import Std::Io::{Print, PrintLine, 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_StartsWith};
|
||||
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
||||
import Config::{ServerConfig};
|
||||
import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse};
|
||||
import Errors::{ParseResult};
|
||||
import Parser::{ParseRequest};
|
||||
import Router::{Router, Router_Dispatch};
|
||||
|
||||
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_append_int(sb: *void, n: int64);
|
||||
extern func bux_sb_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
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_append_int(sb: *void, n: int64);
|
||||
extern func bux_sb_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
|
||||
pub struct ConnectionTask {
|
||||
fd: int;
|
||||
}
|
||||
|
||||
pub func BuildResponse(resp: HttpResponse) -> String {
|
||||
let sb: *void = bux_sb_new(4096);
|
||||
|
||||
bux_sb_append(sb, "HTTP/1.1 ");
|
||||
bux_sb_append_int(sb, resp.statusCode as int64);
|
||||
bux_sb_append(sb, " ");
|
||||
bux_sb_append(sb, Http_StatusText(resp.statusCode));
|
||||
bux_sb_append(sb, "\r\n");
|
||||
|
||||
bux_sb_append(sb, "Server: Nexus/0.2.0 (Bux)\r\n");
|
||||
|
||||
if bux_strlen(resp.extraHeaders) > 0 {
|
||||
bux_sb_append(sb, resp.extraHeaders);
|
||||
pub struct ConnectionTask {
|
||||
fd: int;
|
||||
}
|
||||
|
||||
if bux_strlen(resp.contentType) > 0 {
|
||||
bux_sb_append(sb, "Content-Type: ");
|
||||
bux_sb_append(sb, resp.contentType);
|
||||
pub func BuildResponse(resp: HttpResponse) -> String {
|
||||
let sb: *void = bux_sb_new(4096);
|
||||
|
||||
bux_sb_append(sb, "HTTP/1.1 ");
|
||||
bux_sb_append_int(sb, resp.statusCode as int64);
|
||||
bux_sb_append(sb, " ");
|
||||
bux_sb_append(sb, Http_StatusText(resp.statusCode));
|
||||
bux_sb_append(sb, "\r\n");
|
||||
|
||||
bux_sb_append(sb, "Server: Nexus/0.2.0 (Bux)\r\n");
|
||||
|
||||
if bux_strlen(resp.extraHeaders) > 0 {
|
||||
bux_sb_append(sb, resp.extraHeaders);
|
||||
}
|
||||
|
||||
if bux_strlen(resp.contentType) > 0 {
|
||||
bux_sb_append(sb, "Content-Type: ");
|
||||
bux_sb_append(sb, resp.contentType);
|
||||
bux_sb_append(sb, "\r\n");
|
||||
}
|
||||
|
||||
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");
|
||||
bux_sb_append(sb, "Connection: close\r\n");
|
||||
bux_sb_append(sb, "\r\n");
|
||||
|
||||
if bodyLen > 0 {
|
||||
bux_sb_append(sb, resp.body);
|
||||
}
|
||||
|
||||
let result: String = bux_sb_build(sb);
|
||||
bux_sb_free(sb);
|
||||
return result;
|
||||
}
|
||||
|
||||
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");
|
||||
bux_sb_append(sb, "Connection: close\r\n");
|
||||
bux_sb_append(sb, "\r\n");
|
||||
pub func HandleConnection(fd: int, router: Router) {
|
||||
let raw: String = Net_Recv(fd, 8192);
|
||||
if String_Len(raw) == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if bodyLen > 0 {
|
||||
bux_sb_append(sb, resp.body);
|
||||
}
|
||||
|
||||
let result: String = bux_sb_build(sb);
|
||||
bux_sb_free(sb);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub func HandleConnection(fd: int, router: Router) {
|
||||
let raw: String = Net_Recv(fd, 8192);
|
||||
if String_Len(raw) == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// HTTP/2 preface detection
|
||||
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
||||
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
||||
// HTTP/2 preface detection
|
||||
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
||||
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
||||
"HTTP/2 detected — full support planned for future release.\r\n");
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
return;
|
||||
}
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: ParseResult = ParseRequest(raw);
|
||||
if parsed.tag == ParseResult_Ok {
|
||||
let req: HttpRequest = parsed.data.Ok_0;
|
||||
let resp: HttpResponse = Router_Dispatch(router, req);
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
} else {
|
||||
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request");
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WorkerCtx {
|
||||
taskQueue: *Channel<ConnectionTask>;
|
||||
router: Router;
|
||||
}
|
||||
|
||||
pub func Worker(ctx: *WorkerCtx) {
|
||||
while true {
|
||||
let task: ConnectionTask = Channel_Recv<ConnectionTask>(ctx.taskQueue);
|
||||
HandleConnection(task.fd, ctx.router);
|
||||
Net_Close(task.fd);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcceptorCtx {
|
||||
serverFd: int;
|
||||
taskQueue: *Channel<ConnectionTask>;
|
||||
}
|
||||
|
||||
pub func Acceptor(ctx: *AcceptorCtx) {
|
||||
while true {
|
||||
let fd: int = Net_Accept(ctx.serverFd);
|
||||
if fd >= 0 {
|
||||
let task: ConnectionTask = ConnectionTask { fd: fd };
|
||||
Channel_Send<ConnectionTask>(ctx.taskQueue, task);
|
||||
let parsed: ParseResult = ParseRequest(raw);
|
||||
if parsed.tag == ParseResult_Ok {
|
||||
let req: HttpRequest = parsed.data.Ok_0;
|
||||
let resp: HttpResponse = Router_Dispatch(router, req);
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
} else {
|
||||
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request");
|
||||
Net_Send(fd, BuildResponse(resp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub func RunServer(config: ServerConfig, router: Router) -> int {
|
||||
PrintLine("================================================");
|
||||
PrintLine(" Nexus HTTP Server v0.2.0");
|
||||
PrintLine(" Production-ready HTTP/1.1 with thread-pool");
|
||||
PrintLine(" Built with Bux");
|
||||
PrintLine("================================================");
|
||||
PrintLine("");
|
||||
|
||||
let serverFd: int = Net_Create();
|
||||
if serverFd < 0 {
|
||||
PrintLine("FATAL: socket() failed");
|
||||
return 1;
|
||||
pub struct WorkerCtx {
|
||||
taskQueue: *Channel<ConnectionTask>;
|
||||
router: Router;
|
||||
}
|
||||
|
||||
if !Net_SetReuse(serverFd) {
|
||||
PrintLine("WARN: SO_REUSEADDR failed");
|
||||
pub func Worker(ctx: *WorkerCtx) {
|
||||
while true {
|
||||
let task: ConnectionTask = Channel_Recv<ConnectionTask>(ctx.taskQueue);
|
||||
HandleConnection(task.fd, ctx.router);
|
||||
Net_Close(task.fd);
|
||||
}
|
||||
}
|
||||
|
||||
if !Net_Bind(serverFd, config.bindAddr, config.port) {
|
||||
Print("FATAL: bind failed: ");
|
||||
PrintLine(Net_LastError());
|
||||
Net_Close(serverFd);
|
||||
return 1;
|
||||
pub struct AcceptorCtx {
|
||||
serverFd: int;
|
||||
taskQueue: *Channel<ConnectionTask>;
|
||||
}
|
||||
|
||||
if !Net_Listen(serverFd, config.backlog) {
|
||||
PrintLine("FATAL: listen() failed");
|
||||
Net_Close(serverFd);
|
||||
return 1;
|
||||
pub func Acceptor(ctx: *AcceptorCtx) {
|
||||
while true {
|
||||
let fd: int = Net_Accept(ctx.serverFd);
|
||||
if fd >= 0 {
|
||||
let task: ConnectionTask = ConnectionTask { fd: fd };
|
||||
Channel_Send<ConnectionTask>(ctx.taskQueue, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Print("Listening on http://");
|
||||
Print(config.bindAddr);
|
||||
Print(":");
|
||||
PrintInt(config.port);
|
||||
PrintLine("");
|
||||
PrintInt(config.workerCount);
|
||||
PrintLine(" worker threads | static: ./public/");
|
||||
PrintLine("Endpoints: / /api/health /api/info /ws");
|
||||
PrintLine("Press Ctrl+C to stop.");
|
||||
PrintLine("");
|
||||
pub func RunServer(config: ServerConfig, router: Router) -> int {
|
||||
PrintLine("================================================");
|
||||
PrintLine(" Nexus HTTP Server v0.2.0");
|
||||
PrintLine(" Production-ready HTTP/1.1 with thread-pool");
|
||||
PrintLine(" Built with Bux");
|
||||
PrintLine("================================================");
|
||||
PrintLine("");
|
||||
|
||||
let taskQueue: Channel<ConnectionTask> = Channel_New<ConnectionTask>(config.backlog as int64);
|
||||
let workerCtx: WorkerCtx = WorkerCtx { taskQueue: &taskQueue, router: router };
|
||||
let acceptorCtx: AcceptorCtx = AcceptorCtx { serverFd: serverFd, taskQueue: &taskQueue };
|
||||
let serverFd: int = Net_Create();
|
||||
if serverFd < 0 {
|
||||
PrintLine("FATAL: socket() failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Spawn workers (main thread will also become one)
|
||||
var i: int = 0;
|
||||
while i < config.workerCount - 1 {
|
||||
spawn Worker(&workerCtx);
|
||||
i = i + 1;
|
||||
if !Net_SetReuse(serverFd) {
|
||||
PrintLine("WARN: SO_REUSEADDR failed");
|
||||
}
|
||||
|
||||
if !Net_Bind(serverFd, config.bindAddr, config.port) {
|
||||
Print("FATAL: bind failed: ");
|
||||
PrintLine(Net_LastError());
|
||||
Net_Close(serverFd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if !Net_Listen(serverFd, config.backlog) {
|
||||
PrintLine("FATAL: listen() failed");
|
||||
Net_Close(serverFd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Print("Listening on http://");
|
||||
Print(config.bindAddr);
|
||||
Print(":");
|
||||
PrintInt(config.port);
|
||||
PrintLine("");
|
||||
PrintInt(config.workerCount);
|
||||
PrintLine(" worker threads | static: ./public/");
|
||||
PrintLine("Endpoints: / /api/health /api/info /ws");
|
||||
PrintLine("Press Ctrl+C to stop.");
|
||||
PrintLine("");
|
||||
|
||||
let taskQueue: Channel<ConnectionTask> = Channel_New<ConnectionTask>(config.backlog as int64);
|
||||
let workerCtx: WorkerCtx = WorkerCtx { taskQueue: &taskQueue, router: router };
|
||||
let acceptorCtx: AcceptorCtx = AcceptorCtx { serverFd: serverFd, taskQueue: &taskQueue };
|
||||
|
||||
// Spawn workers (main thread will also become one)
|
||||
var i: int = 0;
|
||||
while i < config.workerCount - 1 {
|
||||
spawn Worker(&workerCtx);
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
// Spawn acceptor
|
||||
spawn Acceptor(&acceptorCtx);
|
||||
|
||||
// Main thread works too
|
||||
Worker(&workerCtx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Spawn acceptor
|
||||
spawn Acceptor(&acceptorCtx);
|
||||
|
||||
// Main thread works too
|
||||
Worker(&workerCtx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+302
-302
@@ -1,339 +1,339 @@
|
||||
module Main {
|
||||
|
||||
import Std::Io::{PrintLine, Print, PrintInt, ReadFile, WriteFile, FileExists};
|
||||
import Std::Os::{Os_ArgsCount, Os_Args};
|
||||
import Std::String::{String_Len, String_Eq, String_StartsWith, String_Find, String_Slice, String_Offset};
|
||||
import Std::Io::{PrintLine, Print, PrintInt, ReadFile, WriteFile, FileExists};
|
||||
import Std::Os::{Os_ArgsCount, Os_Args};
|
||||
import Std::String::{String_Len, String_Eq, String_StartsWith, String_Find, String_Slice, String_Offset};
|
||||
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
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_char(sb: *void, c: char8);
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
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_char(sb: *void, c: char8);
|
||||
|
||||
func SB_AppendChar(sb: *void, c: int) {
|
||||
bux_sb_append_char(sb, c as char8);
|
||||
}
|
||||
extern func bux_sb_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
extern func bux_alloc(size: uint) -> *void;
|
||||
extern func bux_free(ptr: *void);
|
||||
func SB_AppendChar(sb: *void, c: int) {
|
||||
bux_sb_append_char(sb, c as char8);
|
||||
}
|
||||
extern func bux_sb_build(sb: *void) -> String;
|
||||
extern func bux_sb_free(sb: *void);
|
||||
extern func bux_alloc(size: uint) -> *void;
|
||||
extern func bux_free(ptr: *void);
|
||||
|
||||
struct Database {
|
||||
path: String,
|
||||
sb: *void,
|
||||
loaded: bool,
|
||||
count: uint,
|
||||
}
|
||||
struct Database {
|
||||
path: String,
|
||||
sb: *void,
|
||||
loaded: bool,
|
||||
count: uint,
|
||||
}
|
||||
|
||||
func DB_New(filePath: String) -> Database {
|
||||
var db: Database;
|
||||
db.path = filePath;
|
||||
db.sb = bux_sb_new(4096);
|
||||
db.loaded = false;
|
||||
db.count = 0;
|
||||
return db;
|
||||
}
|
||||
func DB_New(filePath: String) -> Database {
|
||||
var db: Database;
|
||||
db.path = filePath;
|
||||
db.sb = bux_sb_new(4096);
|
||||
db.loaded = false;
|
||||
db.count = 0;
|
||||
return db;
|
||||
}
|
||||
|
||||
func DB_Load(self: *Database) -> bool {
|
||||
if self.loaded { return true; }
|
||||
func DB_Load(self: *Database) -> bool {
|
||||
if self.loaded { return true; }
|
||||
|
||||
if !FileExists(self.path) {
|
||||
if !FileExists(self.path) {
|
||||
self.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
let raw: String = ReadFile(self.path);
|
||||
if String_Len(raw) == 0 || bux_strlen(raw) == 0 {
|
||||
self.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bux_sb_append(self.sb, raw);
|
||||
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
var i: uint = 0;
|
||||
var n: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
n = n + 1;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
self.count = n;
|
||||
self.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
let raw: String = ReadFile(self.path);
|
||||
if String_Len(raw) == 0 || bux_strlen(raw) == 0 {
|
||||
self.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bux_sb_append(self.sb, raw);
|
||||
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
var i: uint = 0;
|
||||
var n: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
n = n + 1;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
self.count = n;
|
||||
self.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
func DB_Get(self: *Database, key: String) -> String {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
var i: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if String_StartsWith(line, key) {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
let keyLen: uint = bux_strlen(key);
|
||||
let valStart: uint = keyLen + 1;
|
||||
let lineLen: uint = bux_strlen(line);
|
||||
if valStart < lineLen {
|
||||
return bux_str_slice(line, valStart, lineLen - valStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
func DB_Set(self: *Database, key: String, value: String) {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
|
||||
var newSb: *void = bux_sb_new(4096);
|
||||
var found: bool = false;
|
||||
var i: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
func DB_Get(self: *Database, key: String) -> String {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
var i: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if String_StartsWith(line, key) {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
bux_sb_append(newSb, key);
|
||||
SB_AppendChar(newSb, 61);
|
||||
bux_sb_append(newSb, value);
|
||||
SB_AppendChar(newSb, 10);
|
||||
found = true;
|
||||
let keyLen: uint = bux_strlen(key);
|
||||
let valStart: uint = keyLen + 1;
|
||||
let lineLen: uint = bux_strlen(line);
|
||||
if valStart < lineLen {
|
||||
return bux_str_slice(line, valStart, lineLen - valStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
func DB_Set(self: *Database, key: String, value: String) {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
|
||||
var newSb: *void = bux_sb_new(4096);
|
||||
var found: bool = false;
|
||||
var i: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
if String_StartsWith(line, key) {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
bux_sb_append(newSb, key);
|
||||
SB_AppendChar(newSb, 61);
|
||||
bux_sb_append(newSb, value);
|
||||
SB_AppendChar(newSb, 10);
|
||||
found = true;
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
i = i + 1;
|
||||
|
||||
if !found {
|
||||
bux_sb_append(newSb, key);
|
||||
SB_AppendChar(newSb, 61);
|
||||
bux_sb_append(newSb, value);
|
||||
SB_AppendChar(newSb, 10);
|
||||
self.count = self.count + 1;
|
||||
}
|
||||
|
||||
bux_sb_free(self.sb);
|
||||
self.sb = newSb;
|
||||
}
|
||||
|
||||
if !found {
|
||||
bux_sb_append(newSb, key);
|
||||
SB_AppendChar(newSb, 61);
|
||||
bux_sb_append(newSb, value);
|
||||
SB_AppendChar(newSb, 10);
|
||||
self.count = self.count + 1;
|
||||
}
|
||||
func DB_Del(self: *Database, key: String) {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
|
||||
bux_sb_free(self.sb);
|
||||
self.sb = newSb;
|
||||
}
|
||||
|
||||
func DB_Del(self: *Database, key: String) {
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
|
||||
var newSb: *void = bux_sb_new(4096);
|
||||
var i: uint = 0;
|
||||
var delCount: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
if String_StartsWith(line, key) {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
delCount = delCount + 1;
|
||||
var newSb: *void = bux_sb_new(4096);
|
||||
var i: uint = 0;
|
||||
var delCount: uint = 0;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
if String_StartsWith(line, key) {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
delCount = delCount + 1;
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
if delCount > 0 {
|
||||
self.count = self.count - delCount;
|
||||
}
|
||||
|
||||
bux_sb_free(self.sb);
|
||||
self.sb = newSb;
|
||||
}
|
||||
|
||||
func DB_Has(self: *Database, key: String) -> bool {
|
||||
let val: String = DB_Get(self, key);
|
||||
return bux_strlen(val) > 0;
|
||||
}
|
||||
|
||||
func DB_Count(self: *Database) -> uint {
|
||||
return self.count;
|
||||
}
|
||||
|
||||
func DB_Keys(self: *Database) -> *String {
|
||||
if self.count == 0 { return null as *String; }
|
||||
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
let keys: *String = bux_alloc(self.count * sizeof(String)) as *String;
|
||||
|
||||
var found: uint = 0;
|
||||
var i: uint = 0;
|
||||
while i < lineCount && found < self.count {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
let keyLen: uint = String_Offset(eqPos, line);
|
||||
keys[found] = bux_str_slice(line, 0, keyLen);
|
||||
found = found + 1;
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
func DB_Save(self: *Database) -> bool {
|
||||
let content: String = bux_sb_build(self.sb);
|
||||
let ok: bool = WriteFile(self.path, content);
|
||||
return ok;
|
||||
}
|
||||
|
||||
func PrintUsage() {
|
||||
PrintLine("SimpleDB — file-backed key-value database");
|
||||
PrintLine("Usage:");
|
||||
PrintLine(" simpledb <dbfile> set <key> <value>");
|
||||
PrintLine(" simpledb <dbfile> get <key>");
|
||||
PrintLine(" simpledb <dbfile> del <key>");
|
||||
PrintLine(" simpledb <dbfile> has <key>");
|
||||
PrintLine(" simpledb <dbfile> keys");
|
||||
PrintLine(" simpledb <dbfile> count");
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let argc: int = Os_ArgsCount();
|
||||
|
||||
if argc < 3 {
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
let dbFile: String = Os_Args(1);
|
||||
let cmd: String = Os_Args(2);
|
||||
|
||||
var db: Database = DB_New(dbFile);
|
||||
|
||||
if String_Eq(cmd, "get") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("get: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let val: String = DB_Get(&db, key);
|
||||
if bux_strlen(val) > 0 {
|
||||
PrintLine(val);
|
||||
} else {
|
||||
bux_sb_append(newSb, line);
|
||||
SB_AppendChar(newSb, 10);
|
||||
Print("(not found) ");
|
||||
PrintLine(key);
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
if delCount > 0 {
|
||||
self.count = self.count - delCount;
|
||||
}
|
||||
|
||||
bux_sb_free(self.sb);
|
||||
self.sb = newSb;
|
||||
}
|
||||
|
||||
func DB_Has(self: *Database, key: String) -> bool {
|
||||
let val: String = DB_Get(self, key);
|
||||
return bux_strlen(val) > 0;
|
||||
}
|
||||
|
||||
func DB_Count(self: *Database) -> uint {
|
||||
return self.count;
|
||||
}
|
||||
|
||||
func DB_Keys(self: *Database) -> *String {
|
||||
if self.count == 0 { return null as *String; }
|
||||
|
||||
let raw: String = bux_sb_build(self.sb);
|
||||
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||
let keys: *String = bux_alloc(self.count * sizeof(String)) as *String;
|
||||
|
||||
var found: uint = 0;
|
||||
var i: uint = 0;
|
||||
while i < lineCount && found < self.count {
|
||||
let line: String = bux_str_split_part(raw, "\n", i);
|
||||
if bux_strlen(line) > 0 {
|
||||
let eqPos: String = String_Find(line, "=");
|
||||
if String_Len(eqPos) > 0 {
|
||||
let keyLen: uint = String_Offset(eqPos, line);
|
||||
keys[found] = bux_str_slice(line, 0, keyLen);
|
||||
found = found + 1;
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
func DB_Save(self: *Database) -> bool {
|
||||
let content: String = bux_sb_build(self.sb);
|
||||
let ok: bool = WriteFile(self.path, content);
|
||||
return ok;
|
||||
}
|
||||
|
||||
func PrintUsage() {
|
||||
PrintLine("SimpleDB — file-backed key-value database");
|
||||
PrintLine("Usage:");
|
||||
PrintLine(" simpledb <dbfile> set <key> <value>");
|
||||
PrintLine(" simpledb <dbfile> get <key>");
|
||||
PrintLine(" simpledb <dbfile> del <key>");
|
||||
PrintLine(" simpledb <dbfile> has <key>");
|
||||
PrintLine(" simpledb <dbfile> keys");
|
||||
PrintLine(" simpledb <dbfile> count");
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let argc: int = Os_ArgsCount();
|
||||
|
||||
if argc < 3 {
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
let dbFile: String = Os_Args(1);
|
||||
let cmd: String = Os_Args(2);
|
||||
|
||||
var db: Database = DB_New(dbFile);
|
||||
|
||||
if String_Eq(cmd, "get") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("get: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let val: String = DB_Get(&db, key);
|
||||
if bux_strlen(val) > 0 {
|
||||
PrintLine(val);
|
||||
} else {
|
||||
Print("(not found) ");
|
||||
PrintLine(key);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "set") {
|
||||
DB_Load(&db);
|
||||
if argc < 5 {
|
||||
PrintLine("set: missing key or value");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let val: String = Os_Args(4);
|
||||
DB_Set(&db, key, val);
|
||||
let saved: bool = DB_Save(&db);
|
||||
if saved {
|
||||
Print("OK ");
|
||||
Print(key);
|
||||
Print(" = ");
|
||||
PrintLine(val);
|
||||
} else {
|
||||
PrintLine("set: save failed");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "del") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("del: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
DB_Del(&db, key);
|
||||
DB_Save(&db);
|
||||
Print("DEL ");
|
||||
PrintLine(key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "has") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("has: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let found: bool = DB_Has(&db, key);
|
||||
if found {
|
||||
Print("true ");
|
||||
PrintLine(key);
|
||||
} else {
|
||||
Print("false ");
|
||||
PrintLine(key);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "keys") {
|
||||
DB_Load(&db);
|
||||
let n: uint = DB_Count(&db);
|
||||
Print("keys: ");
|
||||
PrintInt(n as int);
|
||||
PrintLine("");
|
||||
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let list: *String = DB_Keys(&db);
|
||||
var i: uint = 0;
|
||||
while i < n {
|
||||
Print(" ");
|
||||
PrintLine(list[i]);
|
||||
i = i + 1;
|
||||
if String_Eq(cmd, "set") {
|
||||
DB_Load(&db);
|
||||
if argc < 5 {
|
||||
PrintLine("set: missing key or value");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let val: String = Os_Args(4);
|
||||
DB_Set(&db, key, val);
|
||||
let saved: bool = DB_Save(&db);
|
||||
if saved {
|
||||
Print("OK ");
|
||||
Print(key);
|
||||
Print(" = ");
|
||||
PrintLine(val);
|
||||
} else {
|
||||
PrintLine("set: save failed");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
bux_free(list as *void);
|
||||
return 0;
|
||||
|
||||
if String_Eq(cmd, "del") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("del: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
DB_Del(&db, key);
|
||||
DB_Save(&db);
|
||||
Print("DEL ");
|
||||
PrintLine(key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "has") {
|
||||
DB_Load(&db);
|
||||
if argc < 4 {
|
||||
PrintLine("has: missing key");
|
||||
return 1;
|
||||
}
|
||||
let key: String = Os_Args(3);
|
||||
let found: bool = DB_Has(&db, key);
|
||||
if found {
|
||||
Print("true ");
|
||||
PrintLine(key);
|
||||
} else {
|
||||
Print("false ");
|
||||
PrintLine(key);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "keys") {
|
||||
DB_Load(&db);
|
||||
let n: uint = DB_Count(&db);
|
||||
Print("keys: ");
|
||||
PrintInt(n as int);
|
||||
PrintLine("");
|
||||
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let list: *String = DB_Keys(&db);
|
||||
var i: uint = 0;
|
||||
while i < n {
|
||||
Print(" ");
|
||||
PrintLine(list[i]);
|
||||
i = i + 1;
|
||||
}
|
||||
bux_free(list as *void);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "count") {
|
||||
DB_Load(&db);
|
||||
let n: uint = DB_Count(&db);
|
||||
Print("count: ");
|
||||
PrintInt(n as int);
|
||||
PrintLine("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Print("unknown command: ");
|
||||
PrintLine(cmd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if String_Eq(cmd, "count") {
|
||||
DB_Load(&db);
|
||||
let n: uint = DB_Count(&db);
|
||||
Print("count: ");
|
||||
PrintInt(n as int);
|
||||
PrintLine("");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Print("unknown command: ");
|
||||
PrintLine(cmd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user