diff --git a/examples/http_server/cookie.noja b/examples/http_server/cookie.noja new file mode 100644 index 0000000..183b05c --- /dev/null +++ b/examples/http_server/cookie.noja @@ -0,0 +1,107 @@ + +fun isSeparator(char: String) + return char == "(" or char == ")" + or char == "<" or char == ">" + or char == "@" or char == "," + or char == ";" or char == ":" + or char == "\\" or char == '"' + or char == "/" or char == "?" + or char == "[" or char == "]" + or char == "=" or char == " " + or char == "{" or char == "}" + or char == "\t"; + +fun isControl(char: String) { + n = string.ord(char); + return (n >= 0 and n <= 31) or n == 127; +} + +fun isChar(char: String) + return string.ord(char) < 128; + +fun isToken(char: String) + return isChar(char) + and not isControl(char) + and not isSeparator(char); + +fun parseToken(src: String, i: int) { + offset = i; + while i < count(src) and isToken(src[i]): + i = i+1; + length = i - offset; + return string.slice(src, offset, length), i; +} + +fun isCookieOctet(char: String) { + n = string.ord(char); + return n == 33 + or (n >= 35 and n <= 43) + or (n >= 45 and n <= 58) + or (n >= 60 and n <= 91) + or (n >= 93 and n <= 126); +} + +fun parseCookieValue(src: String, i: int) { + + dquote = false; + if i < count(src) and src[i] == '"': { + dquote = true; + i = i+1; + } + + if i == count(src) or not isCookieOctet(src[i]): + return none, none, "Missing value"; + + offset = i; + do + i = i+1; + while i < count(src) and isCookieOctet(src[i]); + length = i - offset; + + if dquote: { + if i == count(src) or src[i] != '"': + return none, none, "Missing closing double quote"; + i = i+1; + } + + return string.slice(src, offset, length), i; +} + +fun parseCookiePair(src: String, i: int) { + + while i < count(src) and src[i] == " ": + i = i+1; + + name, i = parseToken(src, i); + + if i == count(src) or src[i] != "=": + return none, none, none, "Missing \"=\" after name"; + i = i+1; # Skip the "=" + + value, i, error = parseCookieValue(src, i); + if error != none: + return none, none, none, error; + + while i < count(src) and src[i] == " ": + i = i+1; + + if i < count(src) and src[i] == ";": + i = i+1; + + return name, value, i; +} + +return { + + fun parseCookieHeader(src: String) { + cookies = {}; + i = 0; + while i < count(src): { + name, value, i, error = parseCookiePair(src, i); + if error != none: + return none, error; + cookies[name] = value; + } + return cookies; + } +}; \ No newline at end of file diff --git a/examples/http_server/main.noja b/examples/http_server/main.noja index 69c0f56..450c77f 100755 --- a/examples/http_server/main.noja +++ b/examples/http_server/main.noja @@ -1,10 +1,39 @@ -server = import("server.noja"); -request = import("request.noja"); -response = import("response.noja"); +server = import("server.noja"); +request = import("request.noja"); +response = import("response.noja"); +cookie = import("cookie.noja"); +urlencoded = import("urlencoded.noja"); Request = request.Request; +respond = response.new; + +fun getCookie(req: Request, name: String) { + src = req.headers["Cookie"]; + if src == none: + return none; + cookies, error = cookie.parseCookieHeader(src); + if error != none: + return none, error; + return cookies[name]; +} + +fun getSessionID(req: Request) { + session_id, error = getCookie(req, "SESSID"); + if error != none: + return none, error; + session_id = string.toInt(session_id); + return session_id; +} + +fun getUsername(req: Request) { + session_id, error = getSessionID(req); + if error != none: + return none, none, error; + username = session_table[session_id]; + return username, session_id; +} fun loadFile(path: String) { - + stream, error = files.openFile(path, files.READ); if error != none: return none, error; @@ -23,77 +52,209 @@ fun loadFile(path: String) { } while num_bytes == size; - files.close(stream); + #files.close(stream); return text; } -fun generateHome() { - title = "Homepage"; - content = "Hello, world!"; - return string.cat( - "", - "", - "", title, "", - "", - "", - "", content, "", - "", - "" - ); +user_table = {}; +session_table = {}; + +fun redirect(endpoint) { + fun callback() + return response.new(307, none, {'Location': endpoint}); + return callback; } -fun generateLogout() { -} +route_table = { + '/': { + GET: redirect("/login") + }, + '/home': { + fun GET(req: Request) { + + username, _, error = getUsername(req); + if error != none: + return respond(400, error); + if username == none: + return respond(401, "You're not logged in"); + + content = string.cat( + "", + "", + "Homepage", + "", + "", + "Hello, ", username, "!", + "", + "" + ); + return respond(200, content); + } + }, + '/login': { + GET: 'pages/login.html', + fun POST(req: Request) { -table = { - '/': 'login.html', - '/home': generateHome, - '/login': 'login.html', - '/signup': 'signup.html', - '/logout': generateLogout + username, _, error = getUsername(req); + if error != none: + return respond(400, error); + if username != none: + return respond(400, "You're already logged in"); + + fun areValid(params) { + expect = {user: String, pass: String}; + return istypeof(expect, params) + and count(params.user) > 0 + and count(params.pass) > 0; + } + + params = urlencoded.parse(req.body); + if not areValid(params): + return respond(400, "Invalid parameters"); + username = params.user; + password = params.pass; + + info = user_table[username]; + + if info == none: + return respond(200, "No such user"); + if info.pass != password: + return respond(200, "Wrong password"); + + # Logged in! + session_id = generateRandomValue(0, 1000); + session_table[session_id] = username; + + message = cat("Welcome, ", username, "!"); + headers = { + "Location" : "/home", + "Set-Cookie": cat("SESSID=", session_id) + }; + return respond(303, message, headers); + } + }, + '/signup': { + + GET: 'pages/signup.html', + + fun POST(req: Request) { + + username, _, error = getUsername(req); + if error != none: + return respond(400, error); + if username != none: + return respond(400, "You're already logged in"); + + fun areValid(params) { + expect = {user: String, pass: String}; + return istypeof(expect, params) + and count(params.user) > 0 + and count(params.pass) > 0; + } + + params = urlencoded.parse(req.body); + if not areValid(params): + return respond(400, "Invalid parameters"); + username = params.user; + password = params.pass; + + if user_table[password] != none: + return respond(200, "User already exists"); + + # Signed up! + user_table[username] = {pass: password}; + session_id = random.generate(0, 1000); + session_table[session_id] = username; + + # Respond + message = string.cat("Welcome, ", username, "!"); + headers = { + "Location" : "/home", + "Set-Cookie": string.cat("SESSID=", toString(session_id)) + }; + return respond(303, message, headers); + } + }, + '/logout': { + fun GET(req: Request) { + + username, session_id, error = getUsername(req); + if error != none: + return respond(400, error); + if username == none: + return respond(401, "You're not logged in"); + + session_table[session_id] = none; + + return respond(303, "Goodbye!", {"Location": "/login"}); + } + } }; -default = 'notfound.html'; +fun join(list: List, glue="") { + if count(list) == 0: + text = ""; + else { + i = 1; + text = toString(list[0]); + while i < count(list): { + text = string.cat(text, glue, toString(list[i])); + i = i+1; + } + } + return text; +} + debug = true; +fun joinPath(a, b) { + u = a[count(a)-1] == '/'; + v = b[count(b)-1] == '/'; + if u and v: + return string.cat(string.slice(a, 0, count(a)-1), b); + if u or v: + return string.cat(a, b); + return string.cat(a, '/', b); +} + +fun getRouteAllowedMethods(route) { + + method_table = route_table[route]; + if method_table == none: + return none; + + allowed = keysof(method_table); + if method_table.HEAD == none and method_table.GET != none: + append(allowed, "HEAD"); + if method_table.OPTIONS == none: + append(allowed, "OPTIONS"); + return allowed; +} + fun callback(req: Request) { - path = req.url.path; + endpoint = req.url.path; + method_table = route_table[endpoint]; + if method_table == none: + return response.new(404); - if path == "/" or path == "/login": { - - if req.method == "POST": { - - - - } else if req.method == "GET"; { - - } else { - assert(false); - } - } - - # Generate response body - { - entry = table[req.url.path]; - if entry == none: { - entry = default; - not_found = true; - } else - not_found = false; - - if isCallable(entry): - data, error = entry(req); + method_item = method_table[req.method]; + if method_item == none: { + allowed = getRouteAllowedMethods(endpoint); + if req.method == "OPTIONS": + status = 204; else - data, error = loadFile(entry); + status = 405; + return response.new(status, none, {"Allow": join(allowed, ", ")}); } - # Generate response object - { - if error == none: + if isCallable(method_item): + res = method_item(req); + else { + file = joinPath(getCurrentWorkingDirectory(), method_item); + data, error = loadFile(file); + if error == none: res = response.new(200, data); - else if not_found: - res = response.new(404, data); else if debug: res = response.new(500, error); else diff --git a/examples/http_server/pages/login.html b/examples/http_server/pages/login.html index bfc32f4..a211aed 100644 --- a/examples/http_server/pages/login.html +++ b/examples/http_server/pages/login.html @@ -1,12 +1,12 @@ - Login page + Log-in page
- + - +
- \ No newline at end of file + diff --git a/examples/http_server/pages/signup.html b/examples/http_server/pages/signup.html new file mode 100644 index 0000000..cbe0971 --- /dev/null +++ b/examples/http_server/pages/signup.html @@ -0,0 +1,12 @@ + + + Sign-up page + + +
+ + + +
+ + diff --git a/examples/http_server/request.noja b/examples/http_server/request.noja index fce8f2c..9d2c4ee 100644 --- a/examples/http_server/request.noja +++ b/examples/http_server/request.noja @@ -22,9 +22,23 @@ Request = { method : String, url : URL, version: Version, - body : None | Buffer | [Buffer, Buffer] + body : String }; +fun bodyBytesToString(body: None | Buffer | [Buffer, Buffer]) { + if type(body) == None: + body = ""; + else if type(body) == Buffer: + body = buffer.toString(body); + else + body = cat(buffer.toString(body[0]), + buffer.toString(body[1])); + return body; +} + +fun printBody(body: None | Buffer | [Buffer, Buffer]) + print(bodyToString(body)); + fun fromSocket(fd: int, max_head: int = 1024) { { @@ -60,9 +74,11 @@ fun fromSocket(fd: int, max_head: int = 1024) { # Receive the remaining portion of the # request's body (if it wasn't already) { - content_length = request.headers['Content-Length']; - if content_length == none: + x = request.headers['Content-Length']; + if x == none: content_length = 0; + else + content_length = string.toInt(string.trim(x)); # TODO: Limit the content length @@ -81,7 +97,7 @@ fun fromSocket(fd: int, max_head: int = 1024) { body_bytes = [body_bytes, second_half]; } } - - request.body = body_bytes; + + request.body = bodyBytesToString(body_bytes); return request; } diff --git a/examples/http_server/server.noja b/examples/http_server/server.noja index dd678bc..9969c06 100644 --- a/examples/http_server/server.noja +++ b/examples/http_server/server.noja @@ -40,7 +40,7 @@ return { port : int = 8080, backlog : int = 32) { - fd, err = net.socket(net.AF_INET, net.SOCK_STREAM, 0); + fd, err = net.socket(net.AF_INET, net.SOCK_STREAM, 0, true); if err != none: return none, err; diff --git a/examples/http_server/urlencoded.noja b/examples/http_server/urlencoded.noja new file mode 100644 index 0000000..404988d --- /dev/null +++ b/examples/http_server/urlencoded.noja @@ -0,0 +1,77 @@ + +fun undoEscaping(src: String) { + assert(src[0] == "%"); + x = src[1]; + y = src[2]; + if not isHex(x) + or not isHex(y): + return none, "Invalid %xx token"; + x = hexToInt(x); + y = hexToInt(y); + byte = x * 16 + y; + char = chr(byte); + return char, i; +} + +fun getStringToken(src: String, i: int) +{ + token = ""; + while i < count(src) + and src[i] != "=" + and src[i] != "&": { + + char = src[i]; + + if char == "%": { + + # Slice the "%xx" token + if i+2 >= count(src): + return none, none, "Invalid %xx escape token"; + slice = string.slice(src, i, 3); + + # Convert it to a character + char, error = undoEscaping(slice); + if error != none: + return none, none, error; + + i = i+3; + } else { + if char == "+": + char = " "; + i = i+1; + } + + token = string.cat(token, char); + } + return token, i; +} + +fun parse(src: String) { + + result = {}; + + i = 0; + while i < count(src): { + + name, i, error = getStringToken(src, i); + if error != none: + return none, error; + + if i < count(src) and src[i] == "=": { + i = i+1; # Skip the "=" + value, i, error = getStringToken(src, i); + if error != none: + return error; + } else + value = none; + + result[name] = value; + + if i < count(src) and src[i] == "&": + i = i+1; + } + + return result; +} + +return {parse: parse}; \ No newline at end of file diff --git a/src/lib/builtins/basic.c b/src/lib/builtins/basic.c index 12ebfa8..a2e7ffb 100644 --- a/src/lib/builtins/basic.c +++ b/src/lib/builtins/basic.c @@ -39,12 +39,28 @@ #include "files.h" #include "string.h" #include "buffer.h" +#include "random.h" #include "../utils/defs.h" #include "../common/defs.h" #include "../objects/objects.h" #include "../runtime/runtime.h" #include "../compiler/compile.h" +static int bin_getCurrentWorkingDirectory(Runtime *runtime, Object **argv, unsigned int argc, Object *rets[static MAX_RETS], Error *error) +{ + UNUSED(argc); + UNUSED(argv); + ASSERT(argc == 0); + + char path[1024]; + if(getcwd(path, sizeof(path)) == NULL) { + Error_Report(error, 1, "Couldn't get current working directory because a buffer is too small"); + return -1; + } + + return returnValues2(error, runtime, rets, "s", path); +} + static int bin_typename(Runtime *runtime, Object **argv, unsigned int argc, Object *rets[static MAX_RETS], Error *error) { UNUSED(argc); @@ -335,6 +351,7 @@ StaticMapSlot bins_basic[] = { { "files", SM_SMAP, .as_smap = bins_files, }, { "buffer", SM_SMAP, .as_smap = bins_buffer, }, { "string", SM_SMAP, .as_smap = bins_string, }, + { "random", SM_SMAP, .as_smap = bins_random, }, { "import", SM_FUNCT, .as_funct = bin_import, .argc = 1, }, { "type", SM_FUNCT, .as_funct = bin_type, .argc = 1 }, @@ -346,5 +363,6 @@ StaticMapSlot bins_basic[] = { { "istypeof", SM_FUNCT, .as_funct = bin_istypeof, .argc = 2, }, { "typename", SM_FUNCT, .as_funct = bin_typename, .argc = 1, }, { "keysof", SM_FUNCT, .as_funct = bin_keysof, .argc = 1, }, + { "getCurrentWorkingDirectory", SM_FUNCT, .as_funct = bin_getCurrentWorkingDirectory, .argc = 0 }, { NULL, SM_END, {}, {} }, }; diff --git a/src/lib/builtins/net.c b/src/lib/builtins/net.c index 0fb0279..a27d6ae 100644 --- a/src/lib/builtins/net.c +++ b/src/lib/builtins/net.c @@ -9,19 +9,27 @@ static int bin_socket(Runtime *runtime, Object **argv, unsigned int argc, Object *rets[static MAX_RETS], Error *error) { - ASSERT(argc == 3); - ParsedArgument pargs[3]; - if (!parseArgs(error, argv, argc, pargs, "iii")) + ASSERT(argc == 4); + ParsedArgument pargs[4]; + if (!parseArgs(error, argv, argc, pargs, "iii?b")) return -1; int domain = pargs[0].as_int; int type = pargs[1].as_int; int proto = pargs[2].as_int; + bool reuse = pargs[3].defined ? pargs[3].as_bool : false; int fd = socket(domain, type, proto); - + if (fd < 0) return returnValues2(error, runtime, rets, "ns", strerror(errno)); + + if (reuse) { + int value = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)) < 0) + return returnValues2(error, runtime, rets, "ns", strerror(errno)); + } + return returnValues2(error, runtime, rets, "i", fd); } @@ -184,7 +192,7 @@ StaticMapSlot bins_net[] = { { "AF_INET", SM_INT, .as_int = AF_INET, }, { "SOCK_STREAM", SM_INT, .as_int = SOCK_STREAM, }, { "SOCK_DGRAM", SM_INT, .as_int = SOCK_DGRAM, }, - { "socket", SM_FUNCT, .as_funct = bin_socket, .argc = 3, }, + { "socket", SM_FUNCT, .as_funct = bin_socket, .argc = 4, }, { "bind", SM_FUNCT, .as_funct = bin_bind, .argc = 4, }, { "listen", SM_FUNCT, .as_funct = bin_listen, .argc = 2, }, { "accept", SM_FUNCT, .as_funct = bin_accept, .argc = 1, }, diff --git a/src/lib/builtins/random.c b/src/lib/builtins/random.c new file mode 100644 index 0000000..d21daf3 --- /dev/null +++ b/src/lib/builtins/random.c @@ -0,0 +1,42 @@ +#include +#include "utils.h" +#include "random.h" +#include "../utils/defs.h" + +static int bin_seed(Runtime *runtime, Object **argv, unsigned int argc, Object *rets[static MAX_RETS], Error *error) +{ + UNUSED(argc); + UNUSED(argv); + ASSERT(argc == 1); + + ParsedArgument pargs[1]; + if (!parseArgs(error, argv, argc, pargs, "i")) + return -1; + + int64_t seed = pargs[0].as_int; + srand(seed); + return returnValues2(error, runtime, rets, "n"); +} + +static int bin_generate(Runtime *runtime, Object **argv, unsigned int argc, Object *rets[static MAX_RETS], Error *error) +{ + UNUSED(argc); + UNUSED(argv); + ASSERT(argc == 2); + + ParsedArgument pargs[2]; + if (!parseArgs(error, argv, argc, pargs, "?i?i")) + return -1; + + int64_t min, max; + if (pargs[0].defined) min = pargs[0].as_int; else min = 0; + if (pargs[1].defined) max = pargs[1].as_int; else max = INT64_MAX; + int64_t value = min + (max - min) * (double) rand() / RAND_MAX; + return returnValues2(error, runtime, rets, "i", value); +} + +StaticMapSlot bins_random[] = { + {"seed", SM_FUNCT, .as_funct = bin_seed, .argc = 1}, + {"generate", SM_FUNCT, .as_funct = bin_generate, .argc = 2}, + { NULL, SM_END, {}, {} }, +}; \ No newline at end of file diff --git a/src/lib/builtins/random.h b/src/lib/builtins/random.h new file mode 100644 index 0000000..5414952 --- /dev/null +++ b/src/lib/builtins/random.h @@ -0,0 +1,2 @@ +#include "../runtime/runtime.h" +extern StaticMapSlot bins_random[]; \ No newline at end of file diff --git a/src/lib/builtins/string.c b/src/lib/builtins/string.c index 05c353b..9f4ce9f 100644 --- a/src/lib/builtins/string.c +++ b/src/lib/builtins/string.c @@ -1,6 +1,8 @@ +#include #include #include #include +#include "utils.h" #include "string.h" #include "../utils/defs.h" #include "../utils/utf8.h" @@ -135,8 +137,138 @@ static int bin_cat(Runtime *runtime, Object **argv, unsigned int argc, Object *r return 1; } +static int bin_slice(Runtime *runtime, Object **argv, unsigned int argc, Object *rets[static MAX_RETS], Error *error) +{ + // 0: string + // 1: start offset or none + // 2: end offset or none + + ParsedArgument pargs[3]; + if (!parseArgs(error, argv, argc, pargs, "s?i?i")) + return -1; + + const char *srcstr = pargs[0].as_string.data; + size_t srclen = pargs[0].as_string.size; + + size_t offset; + size_t length; + + if (pargs[1].defined) { + int64_t n = pargs[1].as_int; + if (n < 0) { + Error_Report(error, 0, "starting offset of string slice must be non-negative"); + return -1; + } + offset = (size_t) n; + } else + offset = 0; + + if (pargs[2].defined) { + int64_t n = pargs[2].as_int; + if (n < 0) { + Error_Report(error, 0, "length of string slice must be non-negative"); + return -1; + } + length = (size_t) n; + } else + length = srclen - offset; + + if (offset > srclen) { + Error_Report(error, 0, "string slice offset is out of bounds"); + return -1; + } + if (offset + length > srclen) { + Error_Report(error, 0, "string slice length is out of bounds"); + return -1; + } + + Heap *heap = Runtime_GetHeap(runtime); + Object *slice = Object_FromString(srcstr + offset, length, heap, error); + if (slice == NULL) + return -1; + rets[0] = slice; + return 1; +} + +static int bin_trim(Runtime *runtime, Object **argv, unsigned int argc, Object *rets[static MAX_RETS], Error *error) +{ + // 0: string + + ParsedArgument pargs[1]; + if (!parseArgs(error, argv, argc, pargs, "s")) + return -1; + + const char *srcstr = pargs[0].as_string.data; + size_t srclen = pargs[0].as_string.size; + + size_t offset = 0; + size_t length = srclen; + + while (offset < srclen && srcstr[offset] == ' ') + offset++; + + Heap *heap = Runtime_GetHeap(runtime); + + Object *result; + if (offset == srclen) + // The string only contained whitespace. + // Return an empty string. + result = Object_FromString("", 0, heap, error); + else { + + // NOTE: The string doesn't contain only + // whitespace, so the length can't + // go under 1. + while (srcstr[offset + length - 1] == ' ') + length--; + + result = Object_FromString(srcstr + offset, length, heap, error); + } + + if (result == NULL) + return -1; + rets[0] = result; + return 1; +} + +static int bin_toInt(Runtime *runtime, Object **argv, unsigned int argc, Object *rets[static MAX_RETS], Error *error) +{ + // 0: string + + ParsedArgument pargs[1]; + if (!parseArgs(error, argv, argc, pargs, "s")) + return -1; + + const char *srcstr = pargs[0].as_string.data; + size_t srclen = pargs[0].as_string.size; + + if (srclen == 0 || !isdigit(srcstr[0])) + return returnValues2(error, runtime, rets, "ns", "String doesn't contain an integer"); + + int64_t result = 0; + size_t i = 0; + do { + int digit = srcstr[i] - '0'; + if (result > (INT64_MAX - digit) / 10) + return returnValues2(error, runtime, rets, "ns", "An overflow occurred"); + result = result * 10 + digit; + i++; + } while (i < srclen && isdigit(srcstr[i])); + + if (i < srclen) + // Found something other than a digit after the + // sequence of digits, therefore the string can't + // be considered an integer. + return returnValues2(error, runtime, rets, "ns", "String doesn't contain an integer"); + + return returnValues2(error, runtime, rets, "i", result); +} + StaticMapSlot bins_string[] = { - { "ord", SM_FUNCT, .as_funct = bin_ord, .argc = 1 }, - { "chr", SM_FUNCT, .as_funct = bin_chr, .argc = 1 }, - { "cat", SM_FUNCT, .as_funct = bin_cat, .argc = -1 }, + { "ord", SM_FUNCT, .as_funct = bin_ord, .argc = 1 }, + { "chr", SM_FUNCT, .as_funct = bin_chr, .argc = 1 }, + { "cat", SM_FUNCT, .as_funct = bin_cat, .argc = -1 }, + { "trim", SM_FUNCT, .as_funct = bin_trim, .argc = 1 }, + { "slice", SM_FUNCT, .as_funct = bin_slice, .argc = 3 }, + { "toInt", SM_FUNCT, .as_funct = bin_toInt, .argc = 1 }, }; diff --git a/src/lib/builtins/utils.c b/src/lib/builtins/utils.c index 6677032..e4b868a 100644 --- a/src/lib/builtins/utils.c +++ b/src/lib/builtins/utils.c @@ -17,6 +17,7 @@ int returnValuesVA(Error *error, Heap *heap, Object *rets[static MAX_RETS], cons case 'i': ret = Object_FromInt (va_arg(va, int), heap, error); break; case 'f': ret = Object_FromFloat(va_arg(va, double), heap, error); break; case 's': ret = Object_FromString(va_arg(va, char*), -1, heap, error); break; + case 'F': ret = Object_FromStream(va_arg(va, FILE*), heap, error); break; default: Error_Report(error, 1, "Invalid format specifier '%c'", fmt[i]); return -1; @@ -138,8 +139,17 @@ bool parseArgs(Error *error, break; } + case 'F': /* File */ + if (!Object_IsFile(arg)) { + Error_Report(error, 0, "Argument %d was expected to be a file, but a %s was provided", current_arg+1, arg->type->name); + return false; + } + pargs[current_arg].defined = true; + pargs[current_arg].as_file = Object_GetStream(arg); + break; + default: - Error_Report(error, 1, "Invalid argument parser format specifier"); + Error_Report(error, 1, "Invalid argument parser format specifier '%c'", fmt[i]); return false; } } diff --git a/src/lib/compiler/parse.c b/src/lib/compiler/parse.c index 7295df0..34aa331 100644 --- a/src/lib/compiler/parse.c +++ b/src/lib/compiler/parse.c @@ -287,7 +287,7 @@ static Token *tokenize(Source *src, BPAlloc *alloc, Error *error) if(str[i] == '\\') { i += 1; // Consume the \. - if(i < len && (str[i] == '\'' || str[i] == '"')) + if(i < len && (str[i] == '\'' || str[i] == '"' || str[i] == '\\')) i += 1; } else break; diff --git a/src/lib/objects/o_map.c b/src/lib/objects/o_map.c index e7c0793..72aba02 100644 --- a/src/lib/objects/o_map.c +++ b/src/lib/objects/o_map.c @@ -73,6 +73,7 @@ keysof(Object *self, Error *error) { MapObject *map = (MapObject*) self; +#warning "This shouldn't return items that contain none" return Object_NewList2(map->count, map->keys, heap, error); }