diff --git a/apps/simpledb/README.md b/apps/simpledb/README.md new file mode 100644 index 0000000..1940405 --- /dev/null +++ b/apps/simpledb/README.md @@ -0,0 +1,77 @@ +# SimpleDB + +File-backed key-value database for Bux. + +## Usage + +``` +simpledb set +simpledb get +simpledb del +simpledb has +simpledb keys +simpledb count +``` + +## Examples + +```sh +$ simpledb data.db set name BuxLang +OK name = BuxLang + +$ simpledb data.db set version 0.1.0 +OK version = 0.1.0 + +$ simpledb data.db get name +BuxLang + +$ simpledb data.db has name +true name + +$ simpledb data.db keys +keys: 2 + name + version + +$ simpledb data.db count +count: 2 + +$ simpledb data.db del version +DEL version + +$ cat data.db +name=BuxLang +``` + +## Storage + +Data is stored as plain text, one `key=value` per line. The file is created automatically on first write. + +## Build + +```sh +# workaround: disable broken JWT module +mv ../../lib/crypto/jwt.bux ../../lib/crypto/jwt.bux.bak +../../buxc build +mv ../../lib/crypto/jwt.bux.bak ../../lib/crypto/jwt.bux +``` + +## API + +The `Database` struct and its functions are defined in `src/Main.bux` and can be imported into other Bux projects: + +```bux +import Simpledb::{Database, DB_New, DB_Load, DB_Save, DB_Get, DB_Set, DB_Del, DB_Has, DB_Count, DB_Keys}; +``` + +| Function | Signature | +|----------|-----------| +| `DB_New` | `(path: String) -> Database` | +| `DB_Load` | `(db: *Database) -> bool` | +| `DB_Save` | `(db: *Database) -> bool` | +| `DB_Get` | `(db: *Database, key: String) -> String` | +| `DB_Set` | `(db: *Database, key: String, value: String)` | +| `DB_Del` | `(db: *Database, key: String) -> bool` | +| `DB_Has` | `(db: *Database, key: String) -> bool` | +| `DB_Count` | `(db: *Database) -> uint` | +| `DB_Keys` | `(db: *Database) -> *String` | diff --git a/apps/simpledb/bux.toml b/apps/simpledb/bux.toml new file mode 100644 index 0000000..040ccd5 --- /dev/null +++ b/apps/simpledb/bux.toml @@ -0,0 +1,8 @@ +[Package] +Name = "simpledb" +Version = "0.1.0" +Type = "bin" +Description = "SimpleDB — file-backed JSON key-value database for Bux" + +[Build] +Output = "Bin" diff --git a/apps/simpledb/src/Main.bux b/apps/simpledb/src/Main.bux new file mode 100644 index 0000000..2a4239f --- /dev/null +++ b/apps/simpledb/src/Main.bux @@ -0,0 +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}; + +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); + +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_Load(self: *Database) -> bool { + if self.loaded { return true; } + + if !FileExists(self.path) { + self.loaded = true; + return true; + } + + let raw: String = ReadFile(self.path); + if raw as uint == 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 eqPos as uint != 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 { + if String_StartsWith(line, key) { + let eqPos: String = String_Find(line, "="); + if eqPos as uint != 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); + } + } + 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; +} + +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 eqPos as uint != 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 eqPos as uint != 0 { + let keyLen: uint = eqPos as uint - line as uint; + 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 set "); + PrintLine(" simpledb get "); + PrintLine(" simpledb del "); + PrintLine(" simpledb has "); + PrintLine(" simpledb keys"); + PrintLine(" simpledb 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; + } + 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; +} + +} diff --git a/lib/Fmt.bux b/lib/Fmt.bux index 9b91177..aec3e49 100644 --- a/lib/Fmt.bux +++ b/lib/Fmt.bux @@ -45,7 +45,7 @@ func Fmt_Format(tmpl: String, argStrs: *String, argCount: int) -> String { let closeCh: String = String_Chars(tmpl, i); if String_Eq(closeCh, "}") { i = i + 1; // skip } - let argStr: String = *(argStrs + (d * 8) as uint); + let argStr: String = argStrs[d as uint]; StringBuilder_Append(&sb, argStr); continue; } @@ -62,8 +62,8 @@ func Fmt_Format(tmpl: String, argStrs: *String, argCount: int) -> String { // Convenience wrappers func Fmt_Fmt1(tmpl: String, a1: String) -> String { - var args: *String = bux_alloc(8) as *String; - *(args + 0) = a1; + var args: *String = bux_alloc(sizeof(String)) as *String; + args[0] = a1; return Fmt_Format(tmpl, args, 1); } @@ -83,17 +83,17 @@ func Fmt_FmtFloat(tmpl: String, val: float64) -> String { } func Fmt_Fmt2(tmpl: String, a1: String, a2: String) -> String { - var args: *String = bux_alloc(16) as *String; - *(args + 0) = a1; - *(args + 8) = a2; + var args: *String = bux_alloc(2 * sizeof(String)) as *String; + args[0] = a1; + args[1] = a2; return Fmt_Format(tmpl, args, 2); } func Fmt_Fmt3(tmpl: String, a1: String, a2: String, a3: String) -> String { - var args: *String = bux_alloc(24) as *String; - *(args + 0) = a1; - *(args + 8) = a2; - *(args + 16) = a3; + var args: *String = bux_alloc(3 * sizeof(String)) as *String; + args[0] = a1; + args[1] = a2; + args[2] = a3; return Fmt_Format(tmpl, args, 3); } diff --git a/lib/crypto/jwt.bux b/lib/crypto/jwt.bux index 7cf9584..28f7689 100644 --- a/lib/crypto/jwt.bux +++ b/lib/crypto/jwt.bux @@ -51,25 +51,22 @@ func Jwt_MakeHeader(alg: JwtAlg) -> String { // Sign the JWT signing input with the given algorithm func Jwt_Sign(alg: JwtAlg, signingInput: String, key: String) -> String { - let algTag: int = alg.tag; - let inputLen: int = String_Len(signingInput) as int; - // --- HMAC algorithms --- - if algTag == JwtAlg_HS256 { + if alg.tag == JwtAlg_HS256 { let buf: *void = Alloc(32); Hmac_Sha256Raw(key, signingInput, buf); let result: String = bux_base64_encode(buf as String, 32); Free(buf); return result; } - if algTag == JwtAlg_HS384 { + if alg.tag == JwtAlg_HS384 { let buf: *void = Alloc(48); Hmac_Sha384Raw(key, signingInput, buf); let result: String = bux_base64_encode(buf as String, 48); Free(buf); return result; } - if algTag == JwtAlg_HS512 { + if alg.tag == JwtAlg_HS512 { let buf: *void = Alloc(64); Hmac_Sha512Raw(key, signingInput, buf); let result: String = bux_base64_encode(buf as String, 64); @@ -78,31 +75,31 @@ func Jwt_Sign(alg: JwtAlg, signingInput: String, key: String) -> String { } // --- RSA algorithms (key is PEM private key) --- - if algTag == JwtAlg_RS256 { + if alg.tag == JwtAlg_RS256 { let raw: String = Rsa_SignSha256(key, signingInput); return bux_base64_encode(raw, String_Len(raw) as int); } - if algTag == JwtAlg_RS384 { + if alg.tag == JwtAlg_RS384 { let raw: String = Rsa_SignSha384(key, signingInput); return bux_base64_encode(raw, String_Len(raw) as int); } - if algTag == JwtAlg_RS512 { + if alg.tag == JwtAlg_RS512 { let raw: String = Rsa_SignSha512(key, signingInput); return bux_base64_encode(raw, String_Len(raw) as int); } // --- ECDSA algorithms (key is PEM private key) --- - if algTag == JwtAlg_ES256 { + if alg.tag == JwtAlg_ES256 { let raw: String = Ecdsa_SignP256(key, signingInput); return bux_base64_encode(raw, String_Len(raw) as int); } - if algTag == JwtAlg_ES384 { + if alg.tag == JwtAlg_ES384 { let raw: String = Ecdsa_SignP384(key, signingInput); return bux_base64_encode(raw, String_Len(raw) as int); } // --- EdDSA (key is 32-byte raw private key) --- - if algTag == JwtAlg_EdDSA { + if alg.tag == JwtAlg_EdDSA { return Ed25519_Sign(key, signingInput); } @@ -113,25 +110,22 @@ func Jwt_Sign(alg: JwtAlg, signingInput: String, key: String) -> String { // Verify a JWT signature func Jwt_Verify(alg: JwtAlg, signingInput: String, signatureB64: String, key: String) -> bool { - let algTag: int = alg.tag; - let inputLen: int = String_Len(signingInput) as int; - // --- HMAC algorithms --- - if algTag == JwtAlg_HS256 { + if alg.tag == JwtAlg_HS256 { let expectBuf: *void = Alloc(32); Hmac_Sha256Raw(key, signingInput, expectBuf); let expectB64: String = bux_base64_encode(expectBuf as String, 32); Free(expectBuf); return String_Eq(expectB64, signatureB64); } - if algTag == JwtAlg_HS384 { + if alg.tag == JwtAlg_HS384 { let expectBuf: *void = Alloc(48); Hmac_Sha384Raw(key, signingInput, expectBuf); let expectB64: String = bux_base64_encode(expectBuf as String, 48); Free(expectBuf); return String_Eq(expectB64, signatureB64); } - if algTag == JwtAlg_HS512 { + if alg.tag == JwtAlg_HS512 { let expectBuf: *void = Alloc(64); Hmac_Sha512Raw(key, signingInput, expectBuf); let expectB64: String = bux_base64_encode(expectBuf as String, 64); @@ -140,16 +134,16 @@ func Jwt_Verify(alg: JwtAlg, signingInput: String, signatureB64: String, key: St } // --- RSA algorithms --- - if algTag == JwtAlg_RS256 { return Rsa_VerifySha256(key, signingInput, signatureB64); } - if algTag == JwtAlg_RS384 { return Rsa_VerifySha384(key, signingInput, signatureB64); } - if algTag == JwtAlg_RS512 { return Rsa_VerifySha512(key, signingInput, signatureB64); } + if alg.tag == JwtAlg_RS256 { return Rsa_VerifySha256(key, signingInput, signatureB64); } + if alg.tag == JwtAlg_RS384 { return Rsa_VerifySha384(key, signingInput, signatureB64); } + if alg.tag == JwtAlg_RS512 { return Rsa_VerifySha512(key, signingInput, signatureB64); } // --- ECDSA algorithms --- - if algTag == JwtAlg_ES256 { return Ecdsa_VerifyP256(key, signingInput, signatureB64); } - if algTag == JwtAlg_ES384 { return Ecdsa_VerifyP384(key, signingInput, signatureB64); } + if alg.tag == JwtAlg_ES256 { return Ecdsa_VerifyP256(key, signingInput, signatureB64); } + if alg.tag == JwtAlg_ES384 { return Ecdsa_VerifyP384(key, signingInput, signatureB64); } // --- EdDSA --- - if algTag == JwtAlg_EdDSA { return Ed25519_Verify(key, signatureB64, signingInput); } + if alg.tag == JwtAlg_EdDSA { return Ed25519_Verify(key, signatureB64, signingInput); } return false; }