diff --git a/.cell/cell.toml b/.cell/cell.toml index 1e16599f..600c4fce 100644 --- a/.cell/cell.toml +++ b/.cell/cell.toml @@ -1,7 +1,3 @@ -# Cell configuration -# ENet service interval in seconds -ENETSERVICE = 0.05 - -# Reply timeout in seconds -REPLYTIMEOUT = 30 \ No newline at end of file +[dependencies] +extramath = "https://gitea.pockle.world/john/extramath@head" \ No newline at end of file diff --git a/meson.build b/meson.build index 24dddb43..18a5e087 100644 --- a/meson.build +++ b/meson.build @@ -288,7 +288,7 @@ src += [ 'anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c', 'render.c','simplex.c','spline.c', 'transform.c','cell.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_nota.c', 'qjs_soloud.c', 'qjs_sdl.c', 'qjs_sdl_video.c', 'qjs_sdl_surface.c', 'qjs_math.c', 'qjs_geometry.c', 'qjs_transform.c', 'qjs_sprite.c', 'qjs_io.c', 'qjs_fd.c', 'qjs_os.c', 'qjs_actor.c', - 'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.c', 'qjs_rtree.c', 'qjs_spline.c', 'qjs_js.c', 'qjs_debug.c' + 'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.c', 'qjs_rtree.c', 'qjs_spline.c', 'qjs_js.c', 'qjs_debug.c', 'picohttpparser.c' ] # quirc src src += [ diff --git a/scripts/engine.cm b/scripts/engine.cm index b452d23b..1ac997ac 100644 --- a/scripts/engine.cm +++ b/scripts/engine.cm @@ -240,14 +240,10 @@ if (io.exists(configPath)) { var config = toml.decode(configText) // Override defaults with config values - if (config.ENETSERVICE !== undefined) { + if (config.ENETSERVICE !== undefined) ENETSERVICE = config.ENETSERVICE - log.system(`Loaded ENETSERVICE from config: ${ENETSERVICE}`) - } - if (config.REPLYTIMEOUT !== undefined) { + if (config.REPLYTIMEOUT !== undefined) REPLYTIMEOUT = config.REPLYTIMEOUT - log.system(`Loaded REPLYTIMEOUT from config: ${REPLYTIMEOUT}`) - } } catch (err) { log.error(`Failed to load config from ${configPath}: ${err}`) } @@ -756,13 +752,13 @@ if (!prog) throw new Error(cell.args.program + " not found."); // Mount the directory containing the program -var progDir = prog.substring(0, prog.lastIndexOf('/')) +var progDir = io.realdir(prog) + "/" + prog.substring(0, prog.lastIndexOf('/')) if (progDir && progDir !== '.') { io.mount(progDir, "") } var progContent = io.slurp(prog) -var prog_script = `(function ${cell.args.program.name()}_start($_, arg) { ${progContent} })` +var prog_script = `(function ${cell.args.program.name()}_start($_, arg) { var args = arg; ${progContent} })` var val = js.eval(cell.args.program, prog_script)($_, cell.args.arg) if (val) throw new Error('Program must not return anything'); diff --git a/scripts/get.ce b/scripts/get.ce index 0b68565f..c2945a46 100644 --- a/scripts/get.ce +++ b/scripts/get.ce @@ -4,8 +4,10 @@ var io = use('io') var shop = use('shop') if (args.length < 1) { - log.console("Usage: cell get ") - log.console("Example: cell get git.world/jj/mod@v0.6.3") + log.console("Usage: cell get [alias]") + log.console("Examples:") + log.console(" cell get git.world/jj/mod@v0.6.3") + log.console(" cell get git.world/jj/mod (uses head/master)") $_.stop() return } @@ -13,22 +15,30 @@ if (args.length < 1) { var locator = args[0] var parsed = shop.parse_locator(locator) +// If no version specified, append @head if (!parsed) { - log.error("Invalid locator format. Expected: host/owner/name@version") + if (locator.indexOf('@') === -1) { + locator = locator + '@head' + parsed = shop.parse_locator(locator) + } +} + +if (!parsed) { + log.error("Invalid locator format. Expected: host/owner/name[@version]") $_.stop() return } // Initialize shop if needed -if (!io.exists('.cell/shop.toml')) { - log.console("No shop.toml found. Initializing...") +if (!io.exists('.cell/cell.toml')) { + log.console("No cell.toml found. Initializing...") shop.init() } // Load current config var config = shop.load_config() if (!config) { - log.error("Failed to load shop.toml") + log.error("Failed to load cell.toml") $_.stop() return } diff --git a/scripts/help.ce b/scripts/help.ce new file mode 100644 index 00000000..67845cc3 --- /dev/null +++ b/scripts/help.ce @@ -0,0 +1,44 @@ +// cell help - Display help information for cell commands + +var io = use('io') + +var command = args.length > 0 ? args[0] : null + +// Display specific command help +if (command) { + var man_file = 'scripts/man/' + command + '.man' + if (io.exists(man_file)) { + var content = io.slurp(man_file) + log.console(content) + } else { + log.error("No help available for command: " + command) + log.console("Run 'cell help' to see available commands.") + } + $_.stop() + return +} + +// Display general help +var cell_man = 'scripts/man/cell.man' +if (io.exists(cell_man)) { + var content = io.slurp(cell_man) + log.console(content) +} else { + // Fallback if man file doesn't exist + log.console("cell - The Cell module system for Prosperon") + log.console("") + log.console("Usage: cell [arguments]") + log.console("") + log.console("Commands:") + log.console(" init Initialize a new Cell project") + log.console(" get Fetch and add a module dependency") + log.console(" update Update a dependency to a new version") + log.console(" vendor Copy all dependencies locally") + log.console(" build Compile all modules to bytecode") + log.console(" patch Create a patch for a module") + log.console(" help Show this help message") + log.console("") + log.console("Run 'cell help ' for more information on a command.") +} + +$_.stop() \ No newline at end of file diff --git a/scripts/init.ce b/scripts/init.ce index c8f19c24..6aabc220 100644 --- a/scripts/init.ce +++ b/scripts/init.ce @@ -11,13 +11,13 @@ var success = shop.init() if (success) { log.console("Created .cell directory structure:") log.console(" .cell/") - log.console(" ├── shop.toml (manifest)") + log.console(" ├── cell.toml (manifest)") log.console(" ├── lock.toml (checksums)") log.console(" ├── modules/ (vendored source)") log.console(" ├── build/ (compiled modules)") log.console(" └── patches/ (patches)") log.console("") - log.console("Edit .cell/shop.toml to configure your project.") + log.console("Edit .cell/cell.toml to configure your project.") } else { log.error("Failed to initialize .cell directory") } diff --git a/scripts/list.ce b/scripts/list.ce new file mode 100644 index 00000000..1f480e5f --- /dev/null +++ b/scripts/list.ce @@ -0,0 +1,75 @@ + +var shop = use('shop') +var io = use('io') + +// Initialize shop if needed +if (!shop.init()) { + log.error("Failed to initialize .cell directory") + return +} + +// Load configuration +var config = shop.load_config() +if (!config) { + log.console("No modules installed (no cell.toml found)") + return +} + +// List dependencies +if (!config.dependencies || Object.keys(config.dependencies).length === 0) { + log.console("No modules installed") + return +} + +log.console("Installed modules:") +log.console("") + +// Display each dependency +for (var alias in config.dependencies) { + var locator = config.dependencies[alias] + var parsed = shop.parse_locator(locator) + + if (parsed) { + log.console(" " + alias + " -> " + locator) + + // Check if module directory exists + var module_dir = '.cell/modules/' + alias + '@' + parsed.version + if (io.exists(module_dir)) { + log.console(" ✓ Downloaded to " + module_dir) + } else { + log.console(" ✗ Not downloaded (run 'cell get " + locator + "')") + } + + // Check if vendored + var vendor_dir = 'modules/' + alias + '@' + parsed.version + if (io.exists(vendor_dir)) { + log.console(" ✓ Vendored to " + vendor_dir) + } + + // Check if compiled + var build_dir = '.cell/build/' + alias + '@' + parsed.version + if (io.exists(build_dir)) { + log.console(" ✓ Compiled to " + build_dir) + } + } else { + log.console(" " + alias + " -> " + locator + " (invalid locator)") + } + + log.console("") +} + +// Show patches if any +if (config.patches && Object.keys(config.patches).length > 0) { + log.console("Patches:") + for (var alias in config.patches) { + var patch_file = config.patches[alias] + log.console(" " + alias + " -> " + patch_file) + if (io.exists('.cell/' + patch_file)) { + log.console(" ✓ Patch file exists") + } else { + log.console(" ✗ Patch file missing") + } + } +} + +$_.stop() \ No newline at end of file diff --git a/scripts/shop.cm b/scripts/shop.cm index 6b38ad7d..d004e591 100644 --- a/scripts/shop.cm +++ b/scripts/shop.cm @@ -6,18 +6,18 @@ var json = use('json') var Shop = {} -// Load shop.toml configuration +// Load cell.toml configuration Shop.load_config = function() { - var shop_path = '.cell/shop.toml' + var shop_path = '.cell/cell.toml' if (!io.exists(shop_path)) { return null } var content = io.slurp(shop_path) - return toml.parse(content) + return toml.decode(content) } -// Save shop.toml configuration +// Save cell.toml configuration Shop.save_config = function(config) { // Simple TOML writer for our needs var lines = [] @@ -74,7 +74,7 @@ Shop.save_config = function(config) { lines.push(']') } - io.slurpwrite('.cell/shop.toml', lines.join('\n')) + io.slurpwrite('.cell/cell.toml', lines.join('\n')) } // Initialize .cell directory structure @@ -95,7 +95,7 @@ Shop.init = function() { io.mkdir('.cell/patches') } - if (!io.exists('.cell/shop.toml')) { + if (!io.exists('.cell/cell.toml')) { var default_config = { module: "my-game", engine: "mist/prosperon@v0.9.3", @@ -122,7 +122,7 @@ Shop.init = function() { Shop.mount = function() { var config = Shop.load_config() if (!config) { - log.error("No shop.toml found") + log.error("No cell.toml found") return false } @@ -224,7 +224,7 @@ Shop.parse_locator = function(locator) { Shop.add_dependency = function(alias, locator) { var config = Shop.load_config() if (!config) { - log.error("No shop.toml found") + log.error("No cell.toml found") return false } diff --git a/source/blob.h b/source/blob.h index f8a8706f..17cb90f2 100644 --- a/source/blob.h +++ b/source/blob.h @@ -12,29 +12,22 @@ // - stone (immutable): reading is allowed // // The blob is stored as an array of bits in memory, but for simplicity here, -// we store them in a dynamic byte array with a bit_length and capacity in bits. +// we store them in a dynamic byte array with a length and capacity in bits. // ----------------------------------------------------------------------------- typedef struct blob { - // The actual buffer holding the bits (in multiples of 8 bits). uint8_t *data; - // The total number of bits currently in use (the "length" of the blob). - size_t bit_length; - - // The total capacity in bits that 'data' can currently hold without realloc. - size_t bit_capacity; + size_t length; + size_t capacity; // 0 = antestone (mutable) // 1 = stone (immutable) int is_stone; } blob; -// Initialize a new blob -void blob_init(blob *b); - // Create a new blob with specified bit capacity -blob *blob_new(size_t bit_capacity); +blob *blob_new(size_t capacity); // Create a blob from another blob (copy bits from 'from' to 'to') blob *blob_new_from_blob(const blob *src, size_t from, size_t to); @@ -52,22 +45,16 @@ void blob_make_stone(blob *b); int blob_write_bit(blob *b, int bit_val); int blob_write_blob(blob *b, const blob *src); int blob_write_dec64(blob *b, double d); -int blob_write_fit(blob *b, int64_t fit, int length); -int blob_write_kim(blob *b, int64_t value); +int blob_write_int64(blob *b, int64_t i); int blob_write_pad(blob *b, int block_size); int blob_write_text(blob *b, const char *text); -int blob_write_text_raw(blob *b, const char *text); - -int blob_write_bytes(blob *b, void *data, size_t length); // Read operations (only work on stone blobs) int blob_read_bit(const blob *b, size_t pos, int *out_bit); blob *blob_read_blob(const blob *b, size_t from, size_t to); int blob_read_dec64(const blob *b, size_t from, double *out_value); -int blob_read_fit(const blob *b, size_t from, int length, int64_t *out_value); -int blob_read_kim(const blob *b, size_t from, int64_t *out_value, size_t *bits_read); +int blob_read_int64(const blob *b, size_t from, int length, int64_t *out_value); int blob_read_text(const blob *b, size_t from, char **out_text, size_t *bits_read); -int blob_read_text_raw(const blob *b, size_t from, char **out_text, size_t *bits_read); int blob_pad_check(const blob *b, size_t from, int block_size); // Utility functions @@ -77,346 +64,293 @@ int kim_length_for_fit(int64_t value); // Helper to ensure capacity for writing static int blob_ensure_capacity(blob *b, size_t new_bits) { - size_t need_bits = b->bit_length + new_bits; - if (need_bits <= b->bit_capacity) return 0; + size_t need_bits = b->length + new_bits; + if (need_bits <= b->capacity) return 0; - // Increase capacity (in multiples of bytes). - // We can pick a growth strategy. For demonstration, double it: - size_t new_capacity = b->bit_capacity == 0 ? 64 : b->bit_capacity * 2; + // Grow strategy: double until enough + size_t new_capacity = b->capacity == 0 ? 64 : b->capacity * 2; while (new_capacity < need_bits) new_capacity *= 2; - // Round up new_capacity to a multiple of 8 bits - if (new_capacity % 8) { - new_capacity += 8 - (new_capacity % 8); - } + // Round up to multiple of 8 bits + if (new_capacity % 8) new_capacity += 8 - (new_capacity % 8); size_t new_size_bytes = new_capacity / 8; uint8_t *new_ptr = realloc(b->data, new_size_bytes); - if (!new_ptr) { - return -1; // out of memory - } - // zero-fill the new area (only beyond the old capacity) - size_t old_size_bytes = b->bit_capacity / 8; - if (new_size_bytes > old_size_bytes) { + if (!new_ptr) return -1; + + // Zero-fill newly allocated area + size_t old_size_bytes = b->capacity / 8; + if (new_size_bytes > old_size_bytes) memset(new_ptr + old_size_bytes, 0, new_size_bytes - old_size_bytes); - } + b->data = new_ptr; - b->bit_capacity = new_capacity; + b->capacity = new_capacity; return 0; } -void blob_init(blob *b) { - b->data = NULL; - b->bit_length = 0; - b->bit_capacity = 0; - b->is_stone = 0; +// A simple bit-level memcpy (may handle unaligned bits at both ends) +void bitcpy(unsigned char *dst, int dst_bit_offset, + unsigned char *src, int src_bit_offset, int bit_length) +{ + int dst_byte_offset = dst_bit_offset / 8; + int src_byte_offset = src_bit_offset / 8; + int dst_bit = dst_bit_offset % 8; + int src_bit = src_bit_offset % 8; + int remaining_bits = bit_length; + + if (dst_bit != 0 || src_bit != 0) + { + unsigned char mask = (1 << dst_bit) - 1; + unsigned char src_mask = (1 << (8 - src_bit)) - 1; + unsigned char src_byte = src[src_byte_offset] & (src_mask << src_bit); + dst[dst_byte_offset] = (dst[dst_byte_offset] & ~mask) | + (src_byte >> (src_bit - dst_bit)); + remaining_bits -= (8 - dst_bit); + dst_byte_offset++; + src_byte_offset += (remaining_bits <= 0) ? 0 : 1; + } + + int whole_bytes = remaining_bits / 8; + for (int i = 0; i < whole_bytes; i++) + dst[dst_byte_offset + i] = src[src_byte_offset + i]; + remaining_bits %= 8; + dst_byte_offset += whole_bytes; + src_byte_offset += whole_bytes; + + if (remaining_bits > 0) + { + unsigned char mask = (1 << remaining_bits) - 1; + unsigned char src_byte = src[src_byte_offset] & mask; + dst[dst_byte_offset] = (dst[dst_byte_offset] & ~mask) | src_byte; + } } -blob *blob_new(size_t bit_capacity) { - blob *b = calloc(1, sizeof(blob)); - if (!b) return NULL; - - blob_init(b); - - if (bit_capacity > 0) { - // Round up to multiple of 8 - if (bit_capacity % 8) { - bit_capacity += 8 - (bit_capacity % 8); - } - b->bit_capacity = bit_capacity; - size_t bytes = bit_capacity / 8; - b->data = calloc(bytes, 1); - if (!b->data) { - free(b); - return NULL; +// Fast bit-copy for arbitrary bit ranges (inclusive) from src → dest +void copy_bits_fast(const void *src, void *dest, + int n, /* start bit in src (inclusive) */ + int m, /* end bit in src (inclusive) */ + int x) /* start bit in dest */ +{ + const uint8_t *s = (const uint8_t*)src; + uint8_t *d = (uint8_t*)dest; + + int total_bits = m - n + 1; + if (total_bits <= 0) return; + + int src_bit = n; + int dst_bit = x; + int src_byte = src_bit >> 3; + int dst_byte = dst_bit >> 3; + int src_off = src_bit & 7; + int dst_off = dst_bit & 7; + + // 1) Leading partial byte to align dest + if (dst_off != 0) { + int chunk = 8 - dst_off; + if (chunk > total_bits) chunk = total_bits; + + uint8_t dst_mask = (((1u << chunk) - 1u) << dst_off); + uint16_t wb = (uint16_t)s[src_byte] | ((uint16_t)s[src_byte + 1] << 8); + uint8_t bits = (uint8_t)((wb >> src_off) & ((1u << chunk) - 1u)); + bits <<= dst_off; + d[dst_byte] = (d[dst_byte] & ~dst_mask) | (bits & dst_mask); + + total_bits -= chunk; + src_bit += chunk; + dst_bit += chunk; + src_byte = src_bit >> 3; + dst_byte = dst_bit >> 3; + src_off = src_bit & 7; + dst_off = dst_bit & 7; // now zero + } + + // 2) Copy full bytes + if (total_bits >= 8) { + if (src_off == 0) { + int num_bytes = total_bits >> 3; + memcpy(&d[dst_byte], &s[src_byte], (size_t)num_bytes); + total_bits -= num_bytes << 3; + src_byte += num_bytes; + dst_byte += num_bytes; + } else { + int num_bytes = total_bits >> 3; + for (int i = 0; i < num_bytes; i++) { + uint16_t wb = (uint16_t)s[src_byte + i] | + ((uint16_t)s[src_byte + i + 1] << 8); + d[dst_byte + i] = (uint8_t)((wb >> src_off) & 0xFFu); + } + total_bits -= num_bytes << 3; + src_byte += num_bytes; + dst_byte += num_bytes; } } - + + // 3) Trailing bits (< 8) + if (total_bits > 0) { + uint8_t dst_mask = (1u << total_bits) - 1u; + uint16_t wb = (uint16_t)s[src_byte] | ((uint16_t)s[src_byte + 1] << 8); + uint8_t bits = (uint8_t)((wb >> src_off) & dst_mask); + d[dst_byte] = (d[dst_byte] & ~dst_mask) | (bits & dst_mask); + } +} + +// Copy bits [from..to) from src buffer into the blob at its current length +// (i.e. 'to' is exclusive). Advance blob->length by (to - from). Return 0. +static int blob_copy_bits(blob *dest, const void *src, size_t from, size_t to) { + int n = (int)from; + int m = (int)(to - 1); // inclusive + copy_bits_fast(src, dest->data, n, m, (int)dest->length); + dest->length += (to - from); + return 0; +} + +blob *blob_new(size_t capacity) { + if (capacity < 0) capacity = 0; + blob *b = calloc(1, sizeof(blob)); + if (!b) return NULL; + if (blob_ensure_capacity(b, capacity) < 0) { + free(b); + return NULL; + } return b; } blob *blob_new_from_blob(const blob *src, size_t from, size_t to) { if (!src) return NULL; - if (to > src->bit_length) to = src->bit_length; + if (to > src->length) to = src->length; if (from >= to) return blob_new(0); - + size_t copy_len = to - from; blob *b = blob_new(copy_len); if (!b) return NULL; - - b->bit_length = copy_len; - - // Copy bits - for (size_t i = 0; i < copy_len; i++) { - size_t src_bit_index = from + i; - size_t src_byte = src_bit_index >> 3; - size_t src_off = src_bit_index & 7; - int bit_val = (src->data[src_byte] >> src_off) & 1; - size_t dst_byte = i >> 3; - size_t dst_off = i & 7; - if (bit_val) { - b->data[dst_byte] |= (1 << dst_off); - } - } - + + b->length = 0; // will be set by blob_copy_bits + blob_copy_bits(b, src->data, from, to); return b; } blob *blob_new_with_fill(size_t length_bits, int logical_value) { blob *b = blob_new(length_bits); if (!b) return NULL; - - b->bit_length = length_bits; - + + b->length = length_bits; if (logical_value) { - // Fill with 1s - size_t bytes = b->bit_capacity / 8; - memset(b->data, 0xff, bytes); + size_t bytes = b->capacity / 8; + memset(b->data, 0xFF, bytes); // Clear unused bits in last byte - size_t used_bits_in_last_byte = length_bits & 7; - if (used_bits_in_last_byte && bytes > 0) { - uint8_t mask = (1 << used_bits_in_last_byte) - 1; + size_t used_bits = length_bits & 7; + if (used_bits && bytes > 0) { + uint8_t mask = (1 << used_bits) - 1; b->data[bytes - 1] &= mask; } } - return b; } void blob_destroy(blob *b) { - if (b) { - if (b && b->data) { - free(b->data); - b->data = NULL; - b->bit_length = 0; - b->bit_capacity = 0; - } - free(b); + if (!b) return; + if (b->data) { + free(b->data); + b->data = NULL; + b->length = 0; + b->capacity = 0; } + free(b); } void blob_make_stone(blob *b) { if (!b) return; b->is_stone = 1; - // Optionally shrink the buffer to exactly bit_length in size - if (b->bit_capacity > b->bit_length) { - size_t size_in_bytes = (b->bit_length + 7) >> 3; // round up to full bytes - uint8_t *new_ptr = NULL; - if (size_in_bytes) { - new_ptr = realloc(b->data, size_in_bytes); - if (new_ptr) { - b->data = new_ptr; - } + // Optionally shrink buffer to exactly length bits + if (b->capacity > b->length) { + size_t size_bytes = (b->length + 7) >> 3; + if (size_bytes) { + uint8_t *new_ptr = realloc(b->data, size_bytes); + if (new_ptr) b->data = new_ptr; } else { - // zero length free(b->data); b->data = NULL; } - b->bit_capacity = b->bit_length; // capacity in bits now matches length + b->capacity = b->length; } } int blob_write_bit(blob *b, int bit_val) { if (!b || b->is_stone) return -1; - - if (blob_ensure_capacity(b, 1) < 0) { - return -1; - } - - size_t bit_index = b->bit_length; + if (blob_ensure_capacity(b, 1) < 0) return -1; + + size_t bit_index = b->length; size_t byte_index = bit_index >> 3; size_t offset_in_byte = bit_index & 7; - // set or clear bit if (bit_val) b->data[byte_index] |= (1 << offset_in_byte); else b->data[byte_index] &= ~(1 << offset_in_byte); - b->bit_length++; + b->length++; return 0; } int blob_write_blob(blob *b, const blob *src) { if (!b || !src || b->is_stone) return -1; - - // Append all bits from src blob - for (size_t i = 0; i < src->bit_length; i++) { - size_t byte_idx = i / 8; - size_t bit_idx = i % 8; - int bit = (src->data[byte_idx] >> bit_idx) & 1; - if (blob_write_bit(b, bit) < 0) { - return -1; - } - } - - return 0; + return blob_copy_bits(b, src->data, 0, src->length); } int blob_write_dec64(blob *b, double d) { if (!b || b->is_stone) return -1; - - // Simple DEC64 encoding: store as IEEE 754 double (64 bits) - uint64_t bits; - memcpy(&bits, &d, sizeof(bits)); - - // Write 64 bits - for (int i = 0; i < 64; i++) { - if (blob_write_bit(b, (bits >> i) & 1) < 0) { - return -1; - } - } - + if (blob_ensure_capacity(b, 64) < 0) return -1; + copy_bits_fast(&d, b->data, 0, 64 - 1, (int)b->length); + b->length += 64; return 0; } -int blob_write_fit(blob *b, int64_t fit, int length) { +int blob_write_int64(blob *b, int64_t i) { if (!b || b->is_stone) return -1; - if (length < 0 || length > 64) return -1; - - // Check if fit requires more bits than allowed - if (length < 64) { - int64_t max = (1LL << length) - 1; - if (fit < 0 || fit > max) { - return -1; - } - } - - // Write the bits - for (int i = 0; i < length; i++) { - if (blob_write_bit(b, (fit >> i) & 1) < 0) { - return -1; - } - } - - return 0; -} - -// Calculate the kim length in bits for a fit (int64) value -int kim_length_for_fit(int64_t value) { - if (value >= -1 && value <= 127) return 8; - if (value >= -127 && value <= 16383) return 16; - if (value >= -16383 && value <= 2097151) return 24; - if (value >= -2097151 && value <= 268435455) return 32; - if (value >= -268435455 && value <= 34359738367LL) return 40; - if (value >= -34359738367LL && value <= 4398046511103LL) return 48; - if (value >= -4398046511103LL && value <= 562949953421311LL) return 56; - if (value >= -562949953421311LL && value <= 36028797018963967LL) return 64; - if (value >= -36028797018963967LL) return 72; - return 80; // Maximum kim length -} - -int blob_write_kim(blob *b, int64_t value) { - if (!b || b->is_stone) return -1; - - int bits = kim_length_for_fit(value); - int bytes = bits / 8; - - uint8_t kim_bytes[10] = {0}; // Max 10 bytes for kim - - if (value < 0) { - // Negative number: first byte is 0x80, then encode -value-1 - kim_bytes[0] = 0x80; - value = -value - 1; - - // Encode remaining bytes - for (int i = bytes - 1; i > 0; i--) { - kim_bytes[i] = value & 0x7F; - value >>= 7; - if (i > 1) kim_bytes[i] |= 0x80; // Set continuation bit - } - } else { - // Positive number - for (int i = bytes - 1; i >= 0; i--) { - kim_bytes[i] = value & 0x7F; - value >>= 7; - if (i > 0) kim_bytes[i] |= 0x80; // Set continuation bit - } - } - - // Write the kim bytes as bits - for (int i = 0; i < bytes; i++) { - for (int j = 0; j < 8; j++) { - if (blob_write_bit(b, (kim_bytes[i] >> j) & 1) < 0) - return -1; - } - } - + if (blob_ensure_capacity(b, 64) < 0) return -1; + copy_bits_fast(&i, b->data, 0, 64 - 1, (int)b->length); + b->length += 64; return 0; } int blob_write_pad(blob *b, int block_size) { if (!b || b->is_stone) return -1; if (block_size <= 0) return -1; - - // Write a 1 bit - if (blob_write_bit(b, 1) < 0) { - return -1; + + if (blob_write_bit(b, 1) < 0) return -1; + + size_t current = b->length; + size_t rem = current % block_size; + if (rem > 0) { + size_t zeros = block_size - rem; + for (size_t i = 0; i < zeros; i++) + if (blob_write_bit(b, 0) < 0) return -1; } - - // Calculate how many 0 bits to add - size_t current_len = b->bit_length; - size_t remainder = current_len % block_size; - if (remainder > 0) { - size_t zeros_needed = block_size - remainder; - for (size_t i = 0; i < zeros_needed; i++) { - if (blob_write_bit(b, 0) < 0) { - return -1; - } - } - } - return 0; } int blob_write_text(blob *b, const char *text) { if (!b || !text || b->is_stone) return -1; - - size_t len = strlen(text); - - // Write kim-encoded length - if (blob_write_kim(b, len) < 0) { - return -1; - } - - // Write each character as kim-encoded UTF-32 - // For simplicity, assuming ASCII - for (size_t i = 0; i < len; i++) { - if (blob_write_kim(b, (unsigned char)text[i]) < 0) { - return -1; - } - } - - return 0; -} -int blob_write_text_raw(blob *b, const char *text) { - if (!b || !text || b->is_stone) return -1; - size_t len = strlen(text); - return blob_write_bytes(b, (void *)text, len); -} + // Ensure capacity for 64-bit length + len*8 bits of text + if (blob_ensure_capacity(b, 64 + (len * 8)) < 0) return -1; -int blob_write_bytes(blob *b, void *data, size_t length) { - if (!b || !data || b->is_stone) return -1; - - // Write each byte as 8 bits - uint8_t *bytes = (uint8_t *)data; - for (size_t i = 0; i < length; i++) { - for (int j = 0; j < 8; j++) { - if (blob_write_bit(b, (bytes[i] >> j) & 1) < 0) { - return -1; - } - } + // 1) Write length prefix + if (blob_write_int64(b, (int64_t)len) < 0) return -1; + + // 2) Write raw text bytes (len * 8 bits) + if (len > 0) { + copy_bits_fast(text, b->data, 0, (int)(len * 8 - 1), (int)b->length); + b->length += (len * 8); } - return 0; } int blob_read_bit(const blob *b, size_t pos, int *out_bit) { if (!b || !b->is_stone || !out_bit) return -1; - if (pos >= b->bit_length) return -1; - + if (pos >= b->length) return -1; + size_t byte_index = pos >> 3; size_t offset_in_byte = pos & 7; *out_bit = (b->data[byte_index] & (1 << offset_in_byte)) ? 1 : 0; @@ -430,175 +364,65 @@ blob *blob_read_blob(const blob *b, size_t from, size_t to) { int blob_read_dec64(const blob *b, size_t from, double *out_value) { if (!b || !b->is_stone || !out_value) return -1; - if (from + 64 > b->bit_length) return -1; - - // Read 64 bits - uint64_t bits = 0; - for (int i = 0; i < 64; i++) { - int bit; - blob_read_bit(b, from + i, &bit); - if (bit) bits |= (1ULL << i); - } - - // Convert to double - memcpy(out_value, &bits, sizeof(*out_value)); + if (from + 64 > b->length) return -1; + if (from < 0) return -1; + copy_bits_fast(b->data, out_value, (int)from, (int)(from + 64 - 1), 0); return 0; } -int blob_read_fit(const blob *b, size_t from, int length, int64_t *out_value) { +int blob_read_int64(const blob *b, size_t from, int length, int64_t *out_value) { if (!b || !b->is_stone || !out_value) return -1; if (length < 0 || length > 64) return -1; - if (from + length > b->bit_length) return -1; - - // Read bits - int64_t value = 0; - for (int i = 0; i < length; i++) { - int bit; - blob_read_bit(b, from + i, &bit); - if (bit) value |= (1LL << i); - } - - *out_value = value; - return 0; -} - -int blob_read_kim(const blob *b, size_t from, int64_t *out_value, size_t *bits_read) { - if (!b || !b->is_stone || !out_value || !bits_read) return -1; - - size_t pos = from; - uint8_t bytes[10]; - int byte_count = 0; - - // Read bytes until we find one without continuation bit - while (byte_count < 10) { - if (pos + 8 > b->bit_length) return -1; - - uint8_t byte = 0; - for (int i = 0; i < 8; i++) { - int bit; - if (blob_read_bit(b, pos + i, &bit) < 0) - return -1; - if (bit) byte |= (1 << i); - } - - bytes[byte_count++] = byte; - pos += 8; - - if (!(byte & 0x80)) break; // No continuation bit - } - - if (byte_count == 0) return -1; - - // Decode the kim value - int64_t value = 0; - int is_negative = (bytes[0] == 0x80); - - if (is_negative) { - // Skip the 0x80 byte and decode remaining - for (int i = 1; i < byte_count; i++) { - value = (value << 7) | (bytes[i] & 0x7F); - } - value = -value - 1; - } else { - for (int i = 0; i < byte_count; i++) { - value = (value << 7) | (bytes[i] & 0x7F); - } - } - - *out_value = value; - *bits_read = byte_count * 8; + if (from + (size_t)length > b->length) return -1; + copy_bits_fast(b->data, out_value, (int)from, (int)(from + length - 1), 0); return 0; } int blob_read_text(const blob *b, size_t from, char **out_text, size_t *bits_read) { if (!b || !b->is_stone || !out_text || !bits_read) return -1; - if (from >= b->bit_length) return -1; - - // Read kim-encoded length - int64_t length; - size_t len_bits; - if (blob_read_kim(b, from, &length, &len_bits) < 0) { - return -1; - } - - if (length < 0) return -1; - - size_t pos = from + len_bits; - - // Read characters - char *str = malloc(length + 1); - if (!str) return -1; - - for (int64_t i = 0; i < length; i++) { - int64_t ch; - size_t ch_bits; - if (blob_read_kim(b, pos, &ch, &ch_bits) < 0) { - free(str); - return -1; - } - // For simplicity, assuming ASCII - str[i] = (char)(ch & 0xFF); - pos += ch_bits; - } - str[length] = '\0'; - - *out_text = str; - *bits_read = pos - from; - return 0; -} + // Need at least 64 bits for length prefix + if (from + 64 > b->length) return -1; -int blob_read_text_raw(const blob *b, size_t from, char **out_text, size_t *bits_read) { - if (!b || !b->is_stone || !out_text || !bits_read) return -1; - if (from >= b->bit_length) return -1; - - // Check that from is byte-aligned - if (from % 8 != 0) return -1; - - size_t from_byte = from / 8; - size_t length_bytes = (b->bit_length - from) / 8; - - char *str = malloc(length_bytes + 1); + int64_t raw_len = 0; + // Read 64 bits of length + copy_bits_fast(b->data, &raw_len, (int)from, (int)(from + 64 - 1), 0); + if (raw_len < 0) return -1; + size_t len = (size_t)raw_len; + + // Check that there are len bytes following + size_t after_len = from + 64; + size_t needed_end = after_len + (len * 8); + if (needed_end > b->length) return -1; + + // Allocate (len + 1) bytes for C-string + char *str = malloc(len + 1); if (!str) return -1; - - memcpy(str, b->data + from_byte, length_bytes); - str[length_bytes] = '\0'; - + + // Copy exactly len*8 bits into str[0..len-1] + copy_bits_fast(b->data, str, (int)after_len, (int)(after_len + (len * 8) - 1), 0); + str[len] = '\0'; + *out_text = str; - *bits_read = length_bytes * 8; + *bits_read = 64 + (len * 8); return 0; } int blob_pad_check(const blob *b, size_t from, int block_size) { if (!b || !b->is_stone) return 0; if (block_size <= 0) return 0; - - // Check if blob length is multiple of block_size - if (b->bit_length % block_size != 0) { - return 0; - } - - // Check if difference between length and from is <= block_size - int64_t diff = b->bit_length - from; - if (diff <= 0 || diff > block_size) { - return 0; - } - - // Check if bit at from is 1 + + if (b->length % block_size != 0) return 0; + int64_t diff = (int64_t)b->length - (int64_t)from; + if (diff <= 0 || diff > block_size) return 0; + int bit; - if (blob_read_bit(b, from, &bit) < 0 || bit != 1) { - return 0; + if (blob_read_bit(b, from, &bit) < 0 || bit != 1) return 0; + for (size_t i = from + 1; i < b->length; i++) { + if (blob_read_bit(b, i, &bit) < 0 || bit != 0) return 0; } - - // Check if remaining bits are 0 - for (size_t i = from + 1; i < b->bit_length; i++) { - if (blob_read_bit(b, i, &bit) < 0 || bit != 0) { - return 0; - } - } - return 1; } #endif /* BLOB_IMPLEMENTATION */ -#endif /* BLOB_H */ \ No newline at end of file +#endif /* BLOB_H */ diff --git a/source/cell.c b/source/cell.c index debd37ae..fbfca9cb 100644 --- a/source/cell.c +++ b/source/cell.c @@ -448,7 +448,7 @@ void set_actor_state(cell_rt *actor) END: if (actor->state == ACTOR_IDLE && !actor->ar && !has_upcoming) { if (JS_IsUndefined(actor->unneeded)) - actor->ar = SDL_AddTimerNS(SDL_SECONDS_TO_NS(5), actor_remove_cb, actor); + actor->ar = SDL_AddTimerNS(SDL_SECONDS_TO_NS(0.1), actor_remove_cb, actor); else { if (!isinf(actor->unneeded_secs)) actor->ar = SDL_AddTimerNS(SDL_SECONDS_TO_NS(actor->unneeded_secs), actor_remove_cb, actor); diff --git a/source/picohttpparser.c b/source/picohttpparser.c new file mode 100644 index 00000000..680039b4 --- /dev/null +++ b/source/picohttpparser.c @@ -0,0 +1,685 @@ +/* + * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, + * Shigeo Mitsunari + * + * The software is licensed under either the MIT License (below) or the Perl + * license. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include +#include +#include +#ifdef __SSE4_2__ +#ifdef _MSC_VER +#include +#else +#include +#endif +#endif +#include "picohttpparser.h" + +#if __GNUC__ >= 3 +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) +#else +#define likely(x) (x) +#define unlikely(x) (x) +#endif + +#ifdef _MSC_VER +#define ALIGNED(n) _declspec(align(n)) +#else +#define ALIGNED(n) __attribute__((aligned(n))) +#endif + +#define IS_PRINTABLE_ASCII(c) ((unsigned char)(c)-040u < 0137u) + +#define CHECK_EOF() \ + if (buf == buf_end) { \ + *ret = -2; \ + return NULL; \ + } + +#define EXPECT_CHAR_NO_CHECK(ch) \ + if (*buf++ != ch) { \ + *ret = -1; \ + return NULL; \ + } + +#define EXPECT_CHAR(ch) \ + CHECK_EOF(); \ + EXPECT_CHAR_NO_CHECK(ch); + +#define ADVANCE_TOKEN(tok, toklen) \ + do { \ + const char *tok_start = buf; \ + static const char ALIGNED(16) ranges2[16] = "\000\040\177\177"; \ + int found2; \ + buf = findchar_fast(buf, buf_end, ranges2, 4, &found2); \ + if (!found2) { \ + CHECK_EOF(); \ + } \ + while (1) { \ + if (*buf == ' ') { \ + break; \ + } else if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { \ + if ((unsigned char)*buf < '\040' || *buf == '\177') { \ + *ret = -1; \ + return NULL; \ + } \ + } \ + ++buf; \ + CHECK_EOF(); \ + } \ + tok = tok_start; \ + toklen = buf - tok_start; \ + } while (0) + +static const char *token_char_map = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\1\0\1\1\1\1\1\0\0\1\1\0\1\1\0\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0" + "\0\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\1\1" + "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\1\0\1\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + +static const char *findchar_fast(const char *buf, const char *buf_end, const char *ranges, size_t ranges_size, int *found) +{ + *found = 0; +#if __SSE4_2__ + if (likely(buf_end - buf >= 16)) { + __m128i ranges16 = _mm_loadu_si128((const __m128i *)ranges); + + size_t left = (buf_end - buf) & ~15; + do { + __m128i b16 = _mm_loadu_si128((const __m128i *)buf); + int r = _mm_cmpestri(ranges16, ranges_size, b16, 16, _SIDD_LEAST_SIGNIFICANT | _SIDD_CMP_RANGES | _SIDD_UBYTE_OPS); + if (unlikely(r != 16)) { + buf += r; + *found = 1; + break; + } + buf += 16; + left -= 16; + } while (likely(left != 0)); + } +#else + /* suppress unused parameter warning */ + (void)buf_end; + (void)ranges; + (void)ranges_size; +#endif + return buf; +} + +static const char *get_token_to_eol(const char *buf, const char *buf_end, const char **token, size_t *token_len, int *ret) +{ + const char *token_start = buf; + +#ifdef __SSE4_2__ + static const char ALIGNED(16) ranges1[16] = "\0\010" /* allow HT */ + "\012\037" /* allow SP and up to but not including DEL */ + "\177\177"; /* allow chars w. MSB set */ + int found; + buf = findchar_fast(buf, buf_end, ranges1, 6, &found); + if (found) + goto FOUND_CTL; +#else + /* find non-printable char within the next 8 bytes, this is the hottest code; manually inlined */ + while (likely(buf_end - buf >= 8)) { +#define DOIT() \ + do { \ + if (unlikely(!IS_PRINTABLE_ASCII(*buf))) \ + goto NonPrintable; \ + ++buf; \ + } while (0) + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); +#undef DOIT + continue; + NonPrintable: + if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { + goto FOUND_CTL; + } + ++buf; + } +#endif + for (;; ++buf) { + CHECK_EOF(); + if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { + if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { + goto FOUND_CTL; + } + } + } +FOUND_CTL: + if (likely(*buf == '\015')) { + ++buf; + EXPECT_CHAR('\012'); + *token_len = buf - 2 - token_start; + } else if (*buf == '\012') { + *token_len = buf - token_start; + ++buf; + } else { + *ret = -1; + return NULL; + } + *token = token_start; + + return buf; +} + +static const char *is_complete(const char *buf, const char *buf_end, size_t last_len, int *ret) +{ + int ret_cnt = 0; + buf = last_len < 3 ? buf : buf + last_len - 3; + + while (1) { + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + CHECK_EOF(); + EXPECT_CHAR('\012'); + ++ret_cnt; + } else if (*buf == '\012') { + ++buf; + ++ret_cnt; + } else { + ++buf; + ret_cnt = 0; + } + if (ret_cnt == 2) { + return buf; + } + } + + *ret = -2; + return NULL; +} + +#define PARSE_INT(valp_, mul_) \ + if (*buf < '0' || '9' < *buf) { \ + buf++; \ + *ret = -1; \ + return NULL; \ + } \ + *(valp_) = (mul_) * (*buf++ - '0'); + +#define PARSE_INT_3(valp_) \ + do { \ + int res_ = 0; \ + PARSE_INT(&res_, 100) \ + *valp_ = res_; \ + PARSE_INT(&res_, 10) \ + *valp_ += res_; \ + PARSE_INT(&res_, 1) \ + *valp_ += res_; \ + } while (0) + +/* returned pointer is always within [buf, buf_end), or null */ +static const char *parse_token(const char *buf, const char *buf_end, const char **token, size_t *token_len, char next_char, + int *ret) +{ + /* We use pcmpestri to detect non-token characters. This instruction can take no more than eight character ranges (8*2*8=128 + * bits that is the size of a SSE register). Due to this restriction, characters `|` and `~` are handled in the slow loop. */ + static const char ALIGNED(16) ranges[] = "\x00 " /* control chars and up to SP */ + "\"\"" /* 0x22 */ + "()" /* 0x28,0x29 */ + ",," /* 0x2c */ + "//" /* 0x2f */ + ":@" /* 0x3a-0x40 */ + "[]" /* 0x5b-0x5d */ + "{\xff"; /* 0x7b-0xff */ + const char *buf_start = buf; + int found; + buf = findchar_fast(buf, buf_end, ranges, sizeof(ranges) - 1, &found); + if (!found) { + CHECK_EOF(); + } + while (1) { + if (*buf == next_char) { + break; + } else if (!token_char_map[(unsigned char)*buf]) { + *ret = -1; + return NULL; + } + ++buf; + CHECK_EOF(); + } + *token = buf_start; + *token_len = buf - buf_start; + return buf; +} + +/* returned pointer is always within [buf, buf_end), or null */ +static const char *parse_http_version(const char *buf, const char *buf_end, int *minor_version, int *ret) +{ + /* we want at least [HTTP/1.] to try to parse */ + if (buf_end - buf < 9) { + *ret = -2; + return NULL; + } + EXPECT_CHAR_NO_CHECK('H'); + EXPECT_CHAR_NO_CHECK('T'); + EXPECT_CHAR_NO_CHECK('T'); + EXPECT_CHAR_NO_CHECK('P'); + EXPECT_CHAR_NO_CHECK('/'); + EXPECT_CHAR_NO_CHECK('1'); + EXPECT_CHAR_NO_CHECK('.'); + PARSE_INT(minor_version, 1); + return buf; +} + +static const char *parse_headers(const char *buf, const char *buf_end, struct phr_header *headers, size_t *num_headers, + size_t max_headers, int *ret) +{ + for (;; ++*num_headers) { + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + break; + } else if (*buf == '\012') { + ++buf; + break; + } + if (*num_headers == max_headers) { + *ret = -1; + return NULL; + } + if (!(*num_headers != 0 && (*buf == ' ' || *buf == '\t'))) { + /* parsing name, but do not discard SP before colon, see + * http://www.mozilla.org/security/announce/2006/mfsa2006-33.html */ + if ((buf = parse_token(buf, buf_end, &headers[*num_headers].name, &headers[*num_headers].name_len, ':', ret)) == NULL) { + return NULL; + } + if (headers[*num_headers].name_len == 0) { + *ret = -1; + return NULL; + } + ++buf; + for (;; ++buf) { + CHECK_EOF(); + if (!(*buf == ' ' || *buf == '\t')) { + break; + } + } + } else { + headers[*num_headers].name = NULL; + headers[*num_headers].name_len = 0; + } + const char *value; + size_t value_len; + if ((buf = get_token_to_eol(buf, buf_end, &value, &value_len, ret)) == NULL) { + return NULL; + } + /* remove trailing SPs and HTABs */ + const char *value_end = value + value_len; + for (; value_end != value; --value_end) { + const char c = *(value_end - 1); + if (!(c == ' ' || c == '\t')) { + break; + } + } + headers[*num_headers].value = value; + headers[*num_headers].value_len = value_end - value; + } + return buf; +} + +static const char *parse_request(const char *buf, const char *buf_end, const char **method, size_t *method_len, const char **path, + size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, + size_t max_headers, int *ret) +{ + /* skip first empty line (some clients add CRLF after POST content) */ + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + } else if (*buf == '\012') { + ++buf; + } + + /* parse request line */ + if ((buf = parse_token(buf, buf_end, method, method_len, ' ', ret)) == NULL) { + return NULL; + } + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + ADVANCE_TOKEN(*path, *path_len); + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + if (*method_len == 0 || *path_len == 0) { + *ret = -1; + return NULL; + } + if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { + return NULL; + } + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + } else if (*buf == '\012') { + ++buf; + } else { + *ret = -1; + return NULL; + } + + return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); +} + +int phr_parse_request(const char *buf_start, size_t len, const char **method, size_t *method_len, const char **path, + size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len) +{ + const char *buf = buf_start, *buf_end = buf_start + len; + size_t max_headers = *num_headers; + int r; + + *method = NULL; + *method_len = 0; + *path = NULL; + *path_len = 0; + *minor_version = -1; + *num_headers = 0; + + /* if last_len != 0, check if the request is complete (a fast countermeasure + againt slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_request(buf, buf_end, method, method_len, path, path_len, minor_version, headers, num_headers, max_headers, + &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); +} + +static const char *parse_response(const char *buf, const char *buf_end, int *minor_version, int *status, const char **msg, + size_t *msg_len, struct phr_header *headers, size_t *num_headers, size_t max_headers, int *ret) +{ + /* parse "HTTP/1.x" */ + if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { + return NULL; + } + /* skip space */ + if (*buf != ' ') { + *ret = -1; + return NULL; + } + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + /* parse status code, we want at least [:digit:][:digit:][:digit:] to try to parse */ + if (buf_end - buf < 4) { + *ret = -2; + return NULL; + } + PARSE_INT_3(status); + + /* get message including preceding space */ + if ((buf = get_token_to_eol(buf, buf_end, msg, msg_len, ret)) == NULL) { + return NULL; + } + if (*msg_len == 0) { + /* ok */ + } else if (**msg == ' ') { + /* Remove preceding space. Successful return from `get_token_to_eol` guarantees that we would hit something other than SP + * before running past the end of the given buffer. */ + do { + ++*msg; + --*msg_len; + } while (**msg == ' '); + } else { + /* garbage found after status code */ + *ret = -1; + return NULL; + } + + return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); +} + +int phr_parse_response(const char *buf_start, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, + struct phr_header *headers, size_t *num_headers, size_t last_len) +{ + const char *buf = buf_start, *buf_end = buf + len; + size_t max_headers = *num_headers; + int r; + + *minor_version = -1; + *status = 0; + *msg = NULL; + *msg_len = 0; + *num_headers = 0; + + /* if last_len != 0, check if the response is complete (a fast countermeasure + against slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_response(buf, buf_end, minor_version, status, msg, msg_len, headers, num_headers, max_headers, &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); +} + +int phr_parse_headers(const char *buf_start, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len) +{ + const char *buf = buf_start, *buf_end = buf + len; + size_t max_headers = *num_headers; + int r; + + *num_headers = 0; + + /* if last_len != 0, check if the response is complete (a fast countermeasure + against slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_headers(buf, buf_end, headers, num_headers, max_headers, &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); +} + +enum { + CHUNKED_IN_CHUNK_SIZE, + CHUNKED_IN_CHUNK_EXT, + CHUNKED_IN_CHUNK_DATA, + CHUNKED_IN_CHUNK_CRLF, + CHUNKED_IN_TRAILERS_LINE_HEAD, + CHUNKED_IN_TRAILERS_LINE_MIDDLE +}; + +static int decode_hex(int ch) +{ + if ('0' <= ch && ch <= '9') { + return ch - '0'; + } else if ('A' <= ch && ch <= 'F') { + return ch - 'A' + 0xa; + } else if ('a' <= ch && ch <= 'f') { + return ch - 'a' + 0xa; + } else { + return -1; + } +} + +ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *_bufsz) +{ + size_t dst = 0, src = 0, bufsz = *_bufsz; + ssize_t ret = -2; /* incomplete */ + + decoder->_total_read += bufsz; + + while (1) { + switch (decoder->_state) { + case CHUNKED_IN_CHUNK_SIZE: + for (;; ++src) { + int v; + if (src == bufsz) + goto Exit; + if ((v = decode_hex(buf[src])) == -1) { + if (decoder->_hex_count == 0) { + ret = -1; + goto Exit; + } + /* the only characters that may appear after the chunk size are BWS, semicolon, or CRLF */ + switch (buf[src]) { + case ' ': + case '\011': + case ';': + case '\012': + case '\015': + break; + default: + ret = -1; + goto Exit; + } + break; + } + if (decoder->_hex_count == sizeof(size_t) * 2) { + ret = -1; + goto Exit; + } + decoder->bytes_left_in_chunk = decoder->bytes_left_in_chunk * 16 + v; + ++decoder->_hex_count; + } + decoder->_hex_count = 0; + decoder->_state = CHUNKED_IN_CHUNK_EXT; + /* fallthru */ + case CHUNKED_IN_CHUNK_EXT: + /* RFC 7230 A.2 "Line folding in chunk extensions is disallowed" */ + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] == '\012') + break; + } + ++src; + if (decoder->bytes_left_in_chunk == 0) { + if (decoder->consume_trailer) { + decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; + break; + } else { + goto Complete; + } + } + decoder->_state = CHUNKED_IN_CHUNK_DATA; + /* fallthru */ + case CHUNKED_IN_CHUNK_DATA: { + size_t avail = bufsz - src; + if (avail < decoder->bytes_left_in_chunk) { + if (dst != src) + memmove(buf + dst, buf + src, avail); + src += avail; + dst += avail; + decoder->bytes_left_in_chunk -= avail; + goto Exit; + } + if (dst != src) + memmove(buf + dst, buf + src, decoder->bytes_left_in_chunk); + src += decoder->bytes_left_in_chunk; + dst += decoder->bytes_left_in_chunk; + decoder->bytes_left_in_chunk = 0; + decoder->_state = CHUNKED_IN_CHUNK_CRLF; + } + /* fallthru */ + case CHUNKED_IN_CHUNK_CRLF: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] != '\015') + break; + } + if (buf[src] != '\012') { + ret = -1; + goto Exit; + } + ++src; + decoder->_state = CHUNKED_IN_CHUNK_SIZE; + break; + case CHUNKED_IN_TRAILERS_LINE_HEAD: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] != '\015') + break; + } + if (buf[src++] == '\012') + goto Complete; + decoder->_state = CHUNKED_IN_TRAILERS_LINE_MIDDLE; + /* fallthru */ + case CHUNKED_IN_TRAILERS_LINE_MIDDLE: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] == '\012') + break; + } + ++src; + decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; + break; + default: + assert(!"decoder is corrupt"); + } + } + +Complete: + ret = bufsz - src; +Exit: + if (dst != src) + memmove(buf + dst, buf + src, bufsz - src); + *_bufsz = dst; + /* if incomplete but the overhead of the chunked encoding is >=100KB and >80%, signal an error */ + if (ret == -2) { + decoder->_total_overhead += bufsz - dst; + if (decoder->_total_overhead >= 100 * 1024 && decoder->_total_read - decoder->_total_overhead < decoder->_total_read / 4) + ret = -1; + } + return ret; +} + +int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder) +{ + return decoder->_state == CHUNKED_IN_CHUNK_DATA; +} + +#undef CHECK_EOF +#undef EXPECT_CHAR +#undef ADVANCE_TOKEN diff --git a/source/picohttpparser.h b/source/picohttpparser.h new file mode 100644 index 00000000..13bc855b --- /dev/null +++ b/source/picohttpparser.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, + * Shigeo Mitsunari + * + * The software is licensed under either the MIT License (below) or the Perl + * license. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef picohttpparser_h +#define picohttpparser_h + +#include +#include + +#ifdef _MSC_VER +#define ssize_t intptr_t +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* contains name and value of a header (name == NULL if is a continuing line + * of a multiline header */ +struct phr_header { + const char *name; + size_t name_len; + const char *value; + size_t value_len; +}; + +/* returns number of bytes consumed if successful, -2 if request is partial, + * -1 if failed */ +int phr_parse_request(const char *buf, size_t len, const char **method, size_t *method_len, const char **path, size_t *path_len, + int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len); + +/* ditto */ +int phr_parse_response(const char *_buf, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, + struct phr_header *headers, size_t *num_headers, size_t last_len); + +/* ditto */ +int phr_parse_headers(const char *buf, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len); + +/* should be zero-filled before start */ +struct phr_chunked_decoder { + size_t bytes_left_in_chunk; /* number of bytes left in current chunk */ + char consume_trailer; /* if trailing headers should be consumed */ + char _hex_count; + char _state; + uint64_t _total_read; + uint64_t _total_overhead; +}; + +/* the function rewrites the buffer given as (buf, bufsz) removing the chunked- + * encoding headers. When the function returns without an error, bufsz is + * updated to the length of the decoded data available. Applications should + * repeatedly call the function while it returns -2 (incomplete) every time + * supplying newly arrived data. If the end of the chunked-encoded data is + * found, the function returns a non-negative number indicating the number of + * octets left undecoded, that starts from the offset returned by `*bufsz`. + * Returns -1 on error. + */ +ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *bufsz); + +/* returns if the chunked decoder is in middle of chunked data */ +int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/source/qjs_blob.c b/source/qjs_blob.c index 134ce3d2..657606a1 100644 --- a/source/qjs_blob.c +++ b/source/qjs_blob.c @@ -53,7 +53,7 @@ static JSValue js_blob_constructor(JSContext *ctx, JSValueConst new_target, // Random function provided - call it for each bit bd = blob_new((size_t)length_bits); if (bd) { - bd->bit_length = length_bits; + bd->length = length_bits; for (size_t i = 0; i < length_bits; i++) { JSValue randval = JS_Call(ctx, argv[1], JS_UNDEFINED, 0, NULL); if (JS_IsException(randval)) { @@ -85,7 +85,7 @@ static JSValue js_blob_constructor(JSContext *ctx, JSValueConst new_target, if (!src) { return JS_ThrowTypeError(ctx, "Blob constructor: argument 1 not a blob"); } - int64_t from = 0, to = (int64_t)src->bit_length; + int64_t from = 0, to = (int64_t)src->length; if (argc >= 2 && JS_IsNumber(argv[1])) { JS_ToInt64(ctx, &from, argv[1]); if (from < 0) from = 0; @@ -93,7 +93,7 @@ static JSValue js_blob_constructor(JSContext *ctx, JSValueConst new_target, if (argc >= 3 && JS_IsNumber(argv[2])) { JS_ToInt64(ctx, &to, argv[2]); if (to < from) to = from; - if (to > (int64_t)src->bit_length) to = (int64_t)src->bit_length; + if (to > (int64_t)src->length) to = (int64_t)src->length; } bd = blob_new_from_blob(src, (size_t)from, (size_t)to); } @@ -162,84 +162,41 @@ static JSValue js_blob_write_blob(JSContext *ctx, JSValueConst this_val, } // blob.write_dec64(number) -static JSValue js_blob_write_dec64(JSContext *ctx, JSValueConst this_val, +static JSValue js_blob_write_number(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { if (argc < 1) { return JS_ThrowTypeError(ctx, "write_dec64(number) requires 1 argument"); } blob *bd = js2blob(ctx, this_val); - if (!bd) { + if (!bd) return JS_ThrowTypeError(ctx, "write_dec64: not called on a blob"); - } // Get the number as a double and convert to DEC64 double d; - if (JS_ToFloat64(ctx, &d, argv[0]) < 0) { + if (JS_ToFloat64(ctx, &d, argv[0]) < 0) return JS_EXCEPTION; - } - if (blob_write_dec64(bd, d) < 0) { + if (blob_write_dec64(bd, d) < 0) return JS_ThrowTypeError(ctx, "write_dec64: cannot write to stone blob or OOM"); - } - - return JS_UNDEFINED; -} - -// blob.write_fit(fit, length) -static JSValue js_blob_write_fit(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - if (argc < 2) { - return JS_ThrowTypeError(ctx, "write_fit(fit, length) requires 2 arguments"); - } - blob *bd = js2blob(ctx, this_val); - if (!bd) { - return JS_ThrowTypeError(ctx, "write_fit: not called on a blob"); - } - - int64_t fit; - int32_t length; - if (JS_ToInt64(ctx, &fit, argv[0]) < 0) return JS_EXCEPTION; - if (JS_ToInt32(ctx, &length, argv[1]) < 0) return JS_EXCEPTION; - - if (blob_write_fit(bd, fit, length) < 0) { - return JS_ThrowTypeError(ctx, "write_fit: cannot write (stone blob, OOM, or value out of range)"); - } return JS_UNDEFINED; } // blob.write_kim(fit) -static JSValue js_blob_write_kim(JSContext *ctx, JSValueConst this_val, +static JSValue js_blob_write_text(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) { + if (argc < 1) return JS_ThrowTypeError(ctx, "write_kim(fit) requires 1 argument"); - } + blob *bd = js2blob(ctx, this_val); - if (!bd) { + if (!bd) return JS_ThrowTypeError(ctx, "write_kim: not called on a blob"); - } // Handle number or single character string - int64_t value; - if (JS_IsString(argv[0])) { - const char *str = JS_ToCString(ctx, argv[0]); - if (!str) return JS_EXCEPTION; - - // Get first UTF-32 character - // For simplicity, assuming ASCII for now - if (strlen(str) == 0) { - JS_FreeCString(ctx, str); - return JS_ThrowTypeError(ctx, "write_kim: empty string"); - } - value = (unsigned char)str[0]; - JS_FreeCString(ctx, str); - } else { - if (JS_ToInt64(ctx, &value, argv[0]) < 0) return JS_EXCEPTION; - } - - if (blob_write_kim(bd, value) < 0) { + const char *str = JS_ToCString(ctx, argv[0]); + + if (blob_write_text(bd, str) < 0) return JS_ThrowTypeError(ctx, "write_kim: cannot write to stone blob or OOM"); - } return JS_UNDEFINED; } @@ -247,68 +204,18 @@ static JSValue js_blob_write_kim(JSContext *ctx, JSValueConst this_val, // blob.write_pad(block_size) static JSValue js_blob_write_pad(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) { + if (argc < 1) return JS_ThrowTypeError(ctx, "write_pad(block_size) requires 1 argument"); - } + blob *bd = js2blob(ctx, this_val); - if (!bd) { + if (!bd) return JS_ThrowTypeError(ctx, "write_pad: not called on a blob"); - } int32_t block_size; if (JS_ToInt32(ctx, &block_size, argv[0]) < 0) return JS_EXCEPTION; - if (blob_write_pad(bd, block_size) < 0) { + if (blob_write_pad(bd, block_size) < 0) return JS_ThrowTypeError(ctx, "write_pad: cannot write (stone blob, OOM, or invalid block size)"); - } - - return JS_UNDEFINED; -} - -// blob.write_text(text) -static JSValue js_blob_write_text(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "write_text(text) requires 1 argument"); - } - blob *bd = js2blob(ctx, this_val); - if (!bd) { - return JS_ThrowTypeError(ctx, "write_text: not called on a blob"); - } - - const char *str = JS_ToCString(ctx, argv[0]); - if (!str) return JS_EXCEPTION; - - int result = blob_write_text(bd, str); - JS_FreeCString(ctx, str); - - if (result < 0) { - return JS_ThrowTypeError(ctx, "write_text: cannot write to stone blob or OOM"); - } - - return JS_UNDEFINED; -} - -// blob.write_text_raw(text) -static JSValue js_blob_write_text_raw(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "write_text_raw(text) requires 1 argument"); - } - blob *bd = js2blob(ctx, this_val); - if (!bd) { - return JS_ThrowTypeError(ctx, "write_text_raw: not called on a blob"); - } - - const char *str = JS_ToCString(ctx, argv[0]); - if (!str) return JS_EXCEPTION; - - int result = blob_write_text_raw(bd, str); - JS_FreeCString(ctx, str); - - if (result < 0) { - return JS_ThrowTypeError(ctx, "write_text_raw: cannot write to stone blob or OOM"); - } return JS_UNDEFINED; } @@ -350,7 +257,7 @@ static JSValue js_blob_read_blob(JSContext *ctx, JSValueConst this_val, } int64_t from = 0; - int64_t to = bd->bit_length; + int64_t to = bd->length; if (argc >= 1) { if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; @@ -358,7 +265,7 @@ static JSValue js_blob_read_blob(JSContext *ctx, JSValueConst this_val, } if (argc >= 2) { if (JS_ToInt64(ctx, &to, argv[1]) < 0) return JS_EXCEPTION; - if (to > (int64_t)bd->bit_length) to = bd->bit_length; + if (to > (int64_t)bd->length) to = bd->length; } blob *new_bd = blob_read_blob(bd, from, to); @@ -370,7 +277,7 @@ static JSValue js_blob_read_blob(JSContext *ctx, JSValueConst this_val, } // blob.read_dec64(from) -static JSValue js_blob_read_dec64(JSContext *ctx, JSValueConst this_val, +static JSValue js_blob_read_number(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { if (argc < 1) { return JS_ThrowTypeError(ctx, "read_dec64(from) requires 1 argument"); @@ -384,10 +291,13 @@ static JSValue js_blob_read_dec64(JSContext *ctx, JSValueConst this_val, return JS_ThrowTypeError(ctx, "read_dec64: blob must be stone"); } - int64_t from; - if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + double from; + if (JS_ToFloat64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + + if (from < 0) return JS_ThrowRangeError(ctx, "read_dec64: out of range"); double d; + printf("reading blob from %g\n", from); if (blob_read_dec64(bd, from, &d) < 0) { return JS_ThrowRangeError(ctx, "read_dec64: out of range"); } @@ -395,65 +305,6 @@ static JSValue js_blob_read_dec64(JSContext *ctx, JSValueConst this_val, return JS_NewFloat64(ctx, d); } -// blob.read_fit(from, length) -static JSValue js_blob_read_fit(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - if (argc < 2) { - return JS_ThrowTypeError(ctx, "read_fit(from, length) requires 2 arguments"); - } - blob *bd = js2blob(ctx, this_val); - if (!bd) { - return JS_ThrowTypeError(ctx, "read_fit: not called on a blob"); - } - - if (!bd->is_stone) { - return JS_ThrowTypeError(ctx, "read_fit: blob must be stone"); - } - - int64_t from; - int32_t length; - if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; - if (JS_ToInt32(ctx, &length, argv[1]) < 0) return JS_EXCEPTION; - - int64_t value; - if (blob_read_fit(bd, from, length, &value) < 0) { - return JS_ThrowRangeError(ctx, "read_fit: out of range or invalid length"); - } - - return JS_NewInt64(ctx, value); -} - -// blob.read_kim(from) -static JSValue js_blob_read_kim(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "read_kim(from) requires 1 argument"); - } - blob *bd = js2blob(ctx, this_val); - if (!bd) { - return JS_ThrowTypeError(ctx, "read_kim: not called on a blob"); - } - - if (!bd->is_stone) { - return JS_ThrowTypeError(ctx, "read_kim: blob must be stone"); - } - - int64_t from; - if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; - - int64_t value; - size_t bits_read; - if (blob_read_kim(bd, from, &value, &bits_read) < 0) { - return JS_ThrowRangeError(ctx, "read_kim: out of range or invalid kim encoding"); - } - - // Return an object with the value and the number of bits read - JSValue obj = JS_NewObject(ctx); - JS_SetPropertyStr(ctx, obj, "value", JS_NewInt64(ctx, value)); - JS_SetPropertyStr(ctx, obj, "bits_read", JS_NewInt64(ctx, bits_read)); - return obj; -} - // blob.read_text(from) static JSValue js_blob_read_text(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -478,44 +329,7 @@ static JSValue js_blob_read_text(JSContext *ctx, JSValueConst this_val, JSValue result = JS_NewString(ctx, text); free(text); - // Return object with text and total bits read - JSValue obj = JS_NewObject(ctx); - JS_SetPropertyStr(ctx, obj, "text", result); - JS_SetPropertyStr(ctx, obj, "bits_read", JS_NewInt64(ctx, bits_read)); - return obj; -} - -// blob.read_text_raw(from) -static JSValue js_blob_read_text_raw(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - blob *bd = js2blob(ctx, this_val); - if (!bd) { - return JS_ThrowTypeError(ctx, "read_text_raw: not called on a blob"); - } - - if (!bd->is_stone) { - return JS_ThrowTypeError(ctx, "read_text_raw: blob must be stone"); - } - - int64_t from = 0; - if (argc >= 1) { - if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; - } - - char *text; - size_t bits_read; - if (blob_read_text_raw(bd, from, &text, &bits_read) < 0) { - return JS_ThrowRangeError(ctx, "read_text_raw: out of range or not byte-aligned"); - } - - JSValue result = JS_NewString(ctx, text); - free(text); - - // Return object with text and total bits read - JSValue obj = JS_NewObject(ctx); - JS_SetPropertyStr(ctx, obj, "text", result); - JS_SetPropertyStr(ctx, obj, "bits_read", JS_NewInt64(ctx, bits_read)); - return obj; + return result; } // blob.pad?(from, block_size) @@ -561,42 +375,7 @@ static JSValue js_blob_get_length(JSContext *ctx, JSValueConst this_val, int mag if (!bd) { return JS_ThrowTypeError(ctx, "length: not called on a blob"); } - return JS_NewInt64(ctx, bd->bit_length); -} - -// Static kim_length function -static JSValue js_blob_kim_length(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "kim_length(value) requires 1 argument"); - } - - if (JS_IsBool(argv[0])) { - return JS_NewInt32(ctx, 1); - } - - if (JS_IsNumber(argv[0])) { - int64_t value; - if (JS_ToInt64(ctx, &value, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewInt32(ctx, kim_length_for_fit(value)); - } - - if (JS_IsString(argv[0])) { - const char *str = JS_ToCString(ctx, argv[0]); - if (!str) return JS_EXCEPTION; - - size_t len = strlen(str); - // Length encoding + each character encoding - int total_bits = kim_length_for_fit(len); - for (size_t i = 0; i < len; i++) { - total_bits += kim_length_for_fit((unsigned char)str[i]); - } - - JS_FreeCString(ctx, str); - return JS_NewInt32(ctx, total_bits); - } - - return JS_NULL; + return JS_NewInt64(ctx, bd->length); } // ----------------------------------------------------------------------------- @@ -607,19 +386,15 @@ static const JSCFunctionListEntry js_blob_funcs[] = { // Write methods JS_CFUNC_DEF("write_bit", 1, js_blob_write_bit), JS_CFUNC_DEF("write_blob", 1, js_blob_write_blob), - JS_CFUNC_DEF("write_dec64", 1, js_blob_write_dec64), - JS_CFUNC_DEF("write_fit", 2, js_blob_write_fit), - JS_CFUNC_DEF("write_kim", 1, js_blob_write_kim), + JS_CFUNC_DEF("write_number", 1, js_blob_write_number), + JS_CFUNC_DEF("write_text", 1, js_blob_write_text), JS_CFUNC_DEF("write_pad", 1, js_blob_write_pad), - JS_CFUNC_DEF("write_text", 1, js_blob_write_text_raw), // Read methods JS_CFUNC_DEF("read_logical", 1, js_blob_read_logical), JS_CFUNC_DEF("read_blob", 2, js_blob_read_blob), - JS_CFUNC_DEF("read_dec64", 1, js_blob_read_dec64), - JS_CFUNC_DEF("read_fit", 2, js_blob_read_fit), - JS_CFUNC_DEF("read_kim", 1, js_blob_read_kim), - JS_CFUNC_DEF("read_text", 1, js_blob_read_text_raw), + JS_CFUNC_DEF("read_number", 1, js_blob_read_number), + JS_CFUNC_DEF("read_text", 1, js_blob_read_text), JS_CFUNC_DEF("pad?", 2, js_blob_pad_q), // Other methods @@ -645,10 +420,6 @@ JSValue js_blob_use(JSContext *js) { JS_SetConstructor(js, ctor, proto); JS_FreeValue(js, proto); - // Add static kim_length method to constructor - JS_SetPropertyStr(js, ctor, "kim_length", - JS_NewCFunction(js, js_blob_kim_length, "kim_length", 1)); - return ctor; } @@ -656,7 +427,7 @@ JSValue js_new_blob_stoned_copy(JSContext *js, void *data, size_t bytes) { blob *b = blob_new(bytes*8); memcpy(b->data, data, bytes); - b->bit_length = bytes * 8; // Set the actual length in bits + b->length = bytes * 8; // Set the actual length in bits blob_make_stone(b); return blob2js(js, b); @@ -668,7 +439,7 @@ void *js_get_blob_data(JSContext *js, size_t *size, JSValue v) if (!b || !b->is_stone) return NULL; - *size = (b->bit_length + 7) / 8; // Return actual byte size based on bit length + *size = (b->length + 7) / 8; // Return actual byte size based on bit length return b->data; } diff --git a/source/qjs_blob.h b/source/qjs_blob.h index ffc656f9..2fdf222e 100644 --- a/source/qjs_blob.h +++ b/source/qjs_blob.h @@ -2,6 +2,7 @@ #define QJS_BLOB_H #include "cell.h" +#include "blob.h" JSValue js_blob_use(JSContext *ctx); diff --git a/source/qjs_debug.c b/source/qjs_debug.c index bdeba160..fa5d6f4d 100644 --- a/source/qjs_debug.c +++ b/source/qjs_debug.c @@ -9,23 +9,6 @@ JSC_CCALL(debug_local_vars, return js_debugger_local_variables(js, js2number(js, JSC_CCALL(debug_fn_info, return js_debugger_fn_info(js, argv[0])) JSC_CCALL(debug_backtrace_fns, return js_debugger_backtrace_fns(js,NULL)) -#include "blob.h" - -void dump_writer(blob *b, const char *buf, size_t len) -{ - blob_write_bytes(b, buf, len); -} - -JSC_CCALL(debug_dump, - JSPrintValueOptions opt = {0}; - opt.show_hidden=1; - blob *write = blob_new(8*100); - JS_PrintValue(js, dump_writer, write, argv[0], &opt); - int bytes = write->bit_length/8; - ret = JS_NewStringLen(js, write->data, bytes); - blob_destroy(write); -) - static const JSCFunctionListEntry js_debug_funcs[] = { MIST_FUNC_DEF(debug, stack_depth, 0), MIST_FUNC_DEF(debug, build_backtrace, 0), @@ -33,7 +16,6 @@ static const JSCFunctionListEntry js_debug_funcs[] = { MIST_FUNC_DEF(debug, local_vars, 1), MIST_FUNC_DEF(debug, fn_info, 1), MIST_FUNC_DEF(debug, backtrace_fns,0), - MIST_FUNC_DEF(debug, dump, 1), }; JSValue js_debug_use(JSContext *js) { diff --git a/source/qjs_http.c b/source/qjs_http.c index 354f37e7..1605adce 100644 --- a/source/qjs_http.c +++ b/source/qjs_http.c @@ -1,953 +1,272 @@ -#include "qjs_http.h" #include "qjs_blob.h" -#include -#include -#include +#include "picohttpparser.h" #include #include #include #include #include -#include +#include +#include +#include +// Simple dynamic buffer for reading the response typedef struct { - char *data; - size_t size; - size_t capacity; + char *data; + size_t size; + size_t capacity; } buffer_t; -typedef struct { - mbedtls_net_context server_fd; - mbedtls_ssl_context ssl; - mbedtls_ssl_config conf; - mbedtls_entropy_context entropy; - mbedtls_ctr_drbg_context ctr_drbg; - int use_ssl; - int headers_complete; - buffer_t header_buf; - size_t content_length; - size_t bytes_read; - int chunked_encoding; - size_t chunk_size; - size_t chunk_remaining; - int chunk_state; // 0: size, 1: data, 2: trailer -} http_connection_t; - -static void buffer_init(buffer_t *buf) { - buf->data = NULL; - buf->size = 0; - buf->capacity = 0; +static void buffer_init(buffer_t *b) { + b->data = NULL; + b->size = 0; + b->capacity = 0; } - -static int buffer_append(buffer_t *buf, const void *data, size_t len) { - if (buf->size + len > buf->capacity) { - size_t new_capacity = buf->capacity ? buf->capacity * 2 : 4096; - while (new_capacity < buf->size + len) { - new_capacity *= 2; - } - char *new_data = realloc(buf->data, new_capacity); - if (!new_data) return -1; - buf->data = new_data; - buf->capacity = new_capacity; - } - memcpy(buf->data + buf->size, data, len); - buf->size += len; - return 0; -} - -static void buffer_free(buffer_t *buf) { - free(buf->data); - buf->data = NULL; - buf->size = 0; - buf->capacity = 0; -} - -// Parse URL into components -static int parse_url(const char *url, char **host, char **port, char **path, int *use_ssl) { - *host = NULL; - *port = NULL; - *path = NULL; - *use_ssl = 0; - - const char *p = url; - - // Parse scheme - if (strncmp(p, "https://", 8) == 0) { - *use_ssl = 1; - p += 8; - } else if (strncmp(p, "http://", 7) == 0) { - *use_ssl = 0; - p += 7; - } else { - return -1; // Invalid scheme - } - - // Find host end - const char *host_start = p; - const char *host_end = strchr(p, '/'); - const char *port_start = strchr(p, ':'); - - if (port_start && (!host_end || port_start < host_end)) { - // Has explicit port - *host = strndup(host_start, port_start - host_start); - port_start++; - if (host_end) { - *port = strndup(port_start, host_end - port_start); - } else { - *port = strdup(port_start); - } - } else { - // No explicit port - if (host_end) { - *host = strndup(host_start, host_end - host_start); - } else { - *host = strdup(host_start); - } - *port = strdup(*use_ssl ? "443" : "80"); - } - - // Path - if (host_end) { - *path = strdup(host_end); - } else { - *path = strdup("/"); - } - - return 0; -} - -// Perform HTTP request over plain socket -static int http_request(const char *host, const char *port, const char *request, size_t request_len, buffer_t *response) { - mbedtls_net_context server_fd; - int ret; - - mbedtls_net_init(&server_fd); - - // Connect to server - if ((ret = mbedtls_net_connect(&server_fd, host, port, MBEDTLS_NET_PROTO_TCP)) != 0) { - mbedtls_net_free(&server_fd); - return ret; - } - - // Send request - size_t written = 0; - while (written < request_len) { - ret = mbedtls_net_send(&server_fd, (unsigned char *)request + written, request_len - written); - if (ret < 0) { - mbedtls_net_free(&server_fd); - return ret; - } - written += ret; - } - - // Read response - unsigned char buf[4096]; - do { - ret = mbedtls_net_recv(&server_fd, buf, sizeof(buf)); - if (ret > 0) { - if (buffer_append(response, buf, ret) < 0) { - mbedtls_net_free(&server_fd); - return -1; - } - } - } while (ret > 0); - - mbedtls_net_free(&server_fd); - return (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) ? 0 : ret; -} - -// Perform HTTPS request over SSL -static int https_request(const char *host, const char *port, const char *request, size_t request_len, buffer_t *response) { - mbedtls_net_context server_fd; - mbedtls_ssl_context ssl; - mbedtls_ssl_config conf; - mbedtls_entropy_context entropy; - mbedtls_ctr_drbg_context ctr_drbg; - const char *pers = "qjs_https_client"; - int ret; - - // Initialize structures - mbedtls_net_init(&server_fd); - mbedtls_ssl_init(&ssl); - mbedtls_ssl_config_init(&conf); - mbedtls_entropy_init(&entropy); - mbedtls_ctr_drbg_init(&ctr_drbg); - - // Seed RNG - if ((ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, - (const unsigned char *)pers, strlen(pers))) != 0) { - goto exit; - } - - // Connect to server - if ((ret = mbedtls_net_connect(&server_fd, host, port, MBEDTLS_NET_PROTO_TCP)) != 0) { - goto exit; - } - - // Configure SSL - if ((ret = mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { - goto exit; - } - - mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg); - mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_OPTIONAL); - - if ((ret = mbedtls_ssl_setup(&ssl, &conf)) != 0) { - goto exit; - } - - if ((ret = mbedtls_ssl_set_hostname(&ssl, host)) != 0) { - goto exit; - } - - mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, NULL); - - // Handshake - while ((ret = mbedtls_ssl_handshake(&ssl)) != 0) { - if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { - goto exit; - } - } - - // Send request - size_t written = 0; - while (written < request_len) { - ret = mbedtls_ssl_write(&ssl, (const unsigned char *)request + written, request_len - written); - if (ret < 0) { - if (ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret != MBEDTLS_ERR_SSL_WANT_READ) { - goto exit; - } - } else { - written += ret; - } - } - - // Read response - unsigned char buf[4096]; - do { - ret = mbedtls_ssl_read(&ssl, buf, sizeof(buf)); - if (ret > 0) { - if (buffer_append(response, buf, ret) < 0) { - ret = -1; - goto exit; - } - } - } while (ret > 0); - - if (ret < 0 && ret != MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY && - ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { - goto exit; - } - - ret = 0; - -exit: - mbedtls_ssl_close_notify(&ssl); - mbedtls_net_free(&server_fd); - mbedtls_ssl_free(&ssl); - mbedtls_ssl_config_free(&conf); - mbedtls_ctr_drbg_free(&ctr_drbg); - mbedtls_entropy_free(&entropy); - - return ret; -} - - -/* --------------------------------------------------------------------- - Very small, non-streaming de-chunker. - Removes the hex size lines and keeps the actual data. - Returns 0 on success, -1 on format errors. - -------------------------------------------------------------------*/ -static int decode_chunked(buffer_t *out, - const char *src, size_t src_len) -{ - printf("[CHUNK] Starting decode_chunked, input len: %zu\n", src_len); - int chunk_count = 0; - - while (src_len) { - /* read hex size line */ - size_t n = 0; - while (n < src_len && src[n] != '\r') { n++; } - if (n == src_len || n + 1 >= src_len || src[n+1] != '\n') { - printf("[CHUNK] Error: incomplete chunk size line\n"); - return -1; - } - - char size_buf[32]; - if (n >= sizeof(size_buf)) { - printf("[CHUNK] Error: chunk size line too long (%zu)\n", n); - return -1; - } - memcpy(size_buf, src, n); size_buf[n] = 0; - - char *endptr; - long chunk_len = strtol(size_buf, &endptr, 16); - if (endptr == size_buf || chunk_len < 0) { - printf("[CHUNK] Error: invalid chunk size '%s'\n", size_buf); - return -1; - } - - printf("[CHUNK] Chunk %d: size=%ld (0x%lx)\n", chunk_count++, chunk_len, chunk_len); - - if (chunk_len == 0) { - printf("[CHUNK] Found terminating chunk, done\n"); - return 0; /* done */ - } - - /* skip "\r\n" */ - src += n + 2; - src_len -= n + 2; - - if ((size_t)chunk_len > src_len) { - printf("[CHUNK] Error: chunk size %ld exceeds remaining data %zu\n", chunk_len, src_len); - return -1; - } - if (buffer_append(out, src, chunk_len) < 0) return -1; - - /* skip chunk data */ - src += chunk_len; - src_len -= chunk_len; - - /* skip trailing \r\n after chunk data */ - if (src_len < 2 || src[0] != '\r' || src[1] != '\n') { - printf("[CHUNK] Error: missing CRLF after chunk data (remaining=%zu)\n", src_len); - if (src_len > 0) { - printf("[CHUNK] Next bytes: "); - for (size_t i = 0; i < 10 && i < src_len; i++) { - printf("\\x%02x", (unsigned char)src[i]); - } - printf("\n"); - } - return -1; - } - src += 2; - src_len -= 2; +static int buffer_append(buffer_t *b, const void *p, size_t len) { + if (b->size + len > b->capacity) { + size_t new_cap = b->capacity ? b->capacity * 2 : 4096; + while (new_cap < b->size + len) new_cap *= 2; + char *nx = realloc(b->data, new_cap); + if (!nx) return -1; + b->data = nx; + b->capacity = new_cap; + } + memcpy(b->data + b->size, p, len); + b->size += len; + return 0; +} +static void buffer_free(buffer_t *b) { + free(b->data); + b->data = NULL; + b->size = 0; + b->capacity = 0; +} + +// Parses "http://host:port/path" or "https://host:port/path" +static int parse_url(const char *url, int *use_ssl, + char **host, char **port, char **path) { + *host = *port = *path = NULL; + *use_ssl = 0; + const char *p = url; + if (strncmp(p, "https://", 8) == 0) { + *use_ssl = 1; + p += 8; + } else if (strncmp(p, "http://", 7) == 0) { + *use_ssl = 0; + p += 7; + } else { + return -1; + } + // find path + const char *path_start = strchr(p, '/'); + size_t hostport_len = path_start ? (size_t)(path_start - p) : strlen(p); + *path = path_start ? strdup(path_start) : strdup("/"); + if (!*path) return -1; + + // check for explicit port + const char *colon = memmem(p, hostport_len, ":", 1); + if (colon) { + size_t hlen = (size_t)(colon - p); + *host = strndup(p, hlen); + size_t plen = hostport_len - hlen - 1; + *port = strndup(colon + 1, plen); + } else { + *host = strndup(p, hostport_len); + *port = strdup(*use_ssl ? "443" : "80"); + } + if (!*host || !*port) { + free(*host); free(*port); free(*path); + return -1; } - printf("[CHUNK] Finished without terminating chunk\n"); return 0; } -// Parse HTTP headers -static int parse_headers(http_connection_t *conn) { - char *headers_end = strstr(conn->header_buf.data, "\r\n\r\n"); - if (!headers_end) { - return 0; // Headers not complete - } - - *headers_end = '\0'; - - // Parse Content-Length - char *content_length = strstr(conn->header_buf.data, "Content-Length:"); - if (!content_length) { - content_length = strstr(conn->header_buf.data, "content-length:"); - } - if (content_length) { - content_length += 15; - while (*content_length == ' ') content_length++; - conn->content_length = strtoul(content_length, NULL, 10); - } - - // Check for chunked encoding - char *transfer_encoding = strstr(conn->header_buf.data, "Transfer-Encoding:"); - if (!transfer_encoding) { - transfer_encoding = strstr(conn->header_buf.data, "transfer-encoding:"); - } - if (transfer_encoding) { - transfer_encoding += 18; - while (*transfer_encoding == ' ') transfer_encoding++; - if (strncmp(transfer_encoding, "chunked", 7) == 0) { - conn->chunked_encoding = 1; - } - } - - conn->headers_complete = 1; - - // Move any body data to the beginning of the buffer - headers_end += 4; - size_t body_len = conn->header_buf.size - (headers_end - conn->header_buf.data); - if (body_len > 0) { - memmove(conn->header_buf.data, headers_end, body_len); - conn->header_buf.size = body_len; - } else { - conn->header_buf.size = 0; - } - - return 1; +// Builds a GET request into buf +static int build_get_request(buffer_t *buf, + const char *host, const char *port, const char *path) { + // HTTP/1.1 requires Host header; Connection: close for simplicity + char tmp[512]; + int n = snprintf(tmp, sizeof(tmp), + "GET %s HTTP/1.1\r\n" + "Host: %s:%s\r\n" + "Connection: close\r\n" + "\r\n", + path, host, port); + if (n < 0) return -1; + return buffer_append(buf, tmp, (size_t)n); } -// Initialize HTTP connection -static void http_connection_init(http_connection_t *conn) { - memset(conn, 0, sizeof(http_connection_t)); - mbedtls_net_init(&conn->server_fd); - mbedtls_ssl_init(&conn->ssl); - mbedtls_ssl_config_init(&conn->conf); - mbedtls_entropy_init(&conn->entropy); - mbedtls_ctr_drbg_init(&conn->ctr_drbg); - buffer_init(&conn->header_buf); -} +// Performs a blocking HTTP GET and returns a QuickJS Blob of the body +static JSValue js_fetch_picoparser(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1 || !JS_IsString(argv[0])) + return JS_ThrowTypeError(ctx, "fetch: URL string required"); -// Free HTTP connection -static void http_connection_free(http_connection_t *conn) { - if (conn->use_ssl) { - mbedtls_ssl_close_notify(&conn->ssl); - } - mbedtls_net_free(&conn->server_fd); - mbedtls_ssl_free(&conn->ssl); - mbedtls_ssl_config_free(&conn->conf); - mbedtls_ctr_drbg_free(&conn->ctr_drbg); - mbedtls_entropy_free(&conn->entropy); - buffer_free(&conn->header_buf); -} + const char *url = JS_ToCString(ctx, argv[0]); + if (!url) return JS_ThrowTypeError(ctx, "fetch: invalid URL"); -// Start HTTP connection -static int http_connection_start(http_connection_t *conn, const char *host, const char *port, - const char *request, size_t request_len, int use_ssl) { - conn->use_ssl = use_ssl; - int ret; - - // Connect to server - if ((ret = mbedtls_net_connect(&conn->server_fd, host, port, MBEDTLS_NET_PROTO_TCP)) != 0) { - return ret; + int use_ssl = 0; + char *host = NULL, *port = NULL, *path = NULL; + if (parse_url(url, &use_ssl, &host, &port, &path) < 0) { + JS_FreeCString(ctx, url); + return JS_ThrowTypeError(ctx, "fetch: URL parse error"); + } + + // Prepare mbedTLS structures + mbedtls_net_context net_ctx; + mbedtls_ssl_context ssl; + mbedtls_ssl_config conf; + mbedtls_ctr_drbg_context drbg; + mbedtls_entropy_context entropy; + mbedtls_net_init(&net_ctx); + mbedtls_ssl_init(&ssl); + mbedtls_ssl_config_init(&conf); + mbedtls_ctr_drbg_init(&drbg); + mbedtls_entropy_init(&entropy); + + int ret; + const char *pers = "js_fetch"; + if (use_ssl) { + if ((ret = mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, + (const unsigned char *)pers, + strlen(pers))) != 0) + goto cleanup_tls; + if ((ret = mbedtls_ssl_config_defaults(&conf, + MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT)) != 0) + goto cleanup_tls; + mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_OPTIONAL); + mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &drbg); + if ((ret = mbedtls_ssl_setup(&ssl, &conf)) != 0) + goto cleanup_tls; + if ((ret = mbedtls_ssl_set_hostname(&ssl, host)) != 0) + goto cleanup_tls; + } + + // Connect to host:port + if ((ret = mbedtls_net_connect(&net_ctx, host, port, + MBEDTLS_NET_PROTO_TCP)) != 0) + goto cleanup_tls; + + if (use_ssl) { + mbedtls_ssl_set_bio(&ssl, &net_ctx, + mbedtls_net_send, mbedtls_net_recv, NULL); + while ((ret = mbedtls_ssl_handshake(&ssl)) != 0) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && + ret != MBEDTLS_ERR_SSL_WANT_WRITE) + goto cleanup_all; } - + } + + // Build and send GET request + buffer_t req = {0}, resp = {0}; + buffer_init(&req); + buffer_init(&resp); + if (build_get_request(&req, host, port, path) < 0) + goto cleanup_all; + + // Write request + size_t off = 0; + while (off < req.size) { if (use_ssl) { - const char *pers = "qjs_https_client"; - - // Seed RNG - if ((ret = mbedtls_ctr_drbg_seed(&conn->ctr_drbg, mbedtls_entropy_func, &conn->entropy, - (const unsigned char *)pers, strlen(pers))) != 0) { - return ret; - } - - // Configure SSL - if ((ret = mbedtls_ssl_config_defaults(&conn->conf, MBEDTLS_SSL_IS_CLIENT, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { - return ret; - } - - mbedtls_ssl_conf_rng(&conn->conf, mbedtls_ctr_drbg_random, &conn->ctr_drbg); - mbedtls_ssl_conf_authmode(&conn->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); - - if ((ret = mbedtls_ssl_setup(&conn->ssl, &conn->conf)) != 0) { - return ret; - } - - if ((ret = mbedtls_ssl_set_hostname(&conn->ssl, host)) != 0) { - return ret; - } - - mbedtls_ssl_set_bio(&conn->ssl, &conn->server_fd, mbedtls_net_send, mbedtls_net_recv, NULL); - - // Handshake - while ((ret = mbedtls_ssl_handshake(&conn->ssl)) != 0) { - if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { - return ret; - } - } - - // Send request - size_t written = 0; - while (written < request_len) { - ret = mbedtls_ssl_write(&conn->ssl, (const unsigned char *)request + written, request_len - written); - if (ret < 0) { - if (ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret != MBEDTLS_ERR_SSL_WANT_READ) { - return ret; - } - } else { - written += ret; - } - } + ret = mbedtls_ssl_write(&ssl, + (const unsigned char *)req.data + off, + req.size - off); + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) + continue; } else { - // Send request for plain HTTP - size_t written = 0; - while (written < request_len) { - ret = mbedtls_net_send(&conn->server_fd, (unsigned char *)request + written, request_len - written); - if (ret < 0) { - return ret; - } - written += ret; - } + ret = mbedtls_net_send(&net_ctx, + (unsigned char *)req.data + off, + req.size - off); } - - return 0; -} + if (ret <= 0) goto cleanup_all; + off += (size_t)ret; + } + buffer_free(&req); -// Read chunk from connection -static int http_connection_read_chunk(http_connection_t *conn, void *buf, size_t max_len, size_t *actual_len) { - int ret; - *actual_len = 0; - - // First, read headers if not complete - if (!conn->headers_complete) { - unsigned char temp[1024]; - - if (conn->use_ssl) { - ret = mbedtls_ssl_read(&conn->ssl, temp, sizeof(temp)); - } else { - ret = mbedtls_net_recv(&conn->server_fd, temp, sizeof(temp)); - } - - if (ret > 0) { - buffer_append(&conn->header_buf, temp, ret); - if (parse_headers(conn)) { - // Headers complete, check if there's body data in the buffer - if (conn->header_buf.size > 0 && max_len > 0) { - size_t to_copy = conn->header_buf.size > max_len ? max_len : conn->header_buf.size; - memcpy(buf, conn->header_buf.data, to_copy); - *actual_len = to_copy; - conn->bytes_read += to_copy; - - // Remove copied data from buffer - if (to_copy < conn->header_buf.size) { - memmove(conn->header_buf.data, conn->header_buf.data + to_copy, - conn->header_buf.size - to_copy); - conn->header_buf.size -= to_copy; - } else { - conn->header_buf.size = 0; - } - } - } - } else if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ) { - return ret; - } - - return 0; - } - - // Headers are complete, read body data - if (conn->chunked_encoding) { - // Handle chunked encoding - simplified for now - // In a full implementation, we'd need to parse chunk sizes - if (conn->use_ssl) { - ret = mbedtls_ssl_read(&conn->ssl, buf, max_len); - } else { - ret = mbedtls_net_recv(&conn->server_fd, buf, max_len); - } + // Read entire response into resp.data + unsigned char buf[4096]; + do { + if (use_ssl) { + ret = mbedtls_ssl_read(&ssl, buf, sizeof(buf)); + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) + continue; } else { - // Regular content-length based reading - size_t remaining = conn->content_length - conn->bytes_read; - if (remaining == 0) { - return 0; // EOF - } - - size_t to_read = remaining > max_len ? max_len : remaining; - - if (conn->use_ssl) { - ret = mbedtls_ssl_read(&conn->ssl, buf, to_read); - } else { - ret = mbedtls_net_recv(&conn->server_fd, buf, to_read); - } + ret = mbedtls_net_recv(&net_ctx, buf, sizeof(buf)); } - if (ret > 0) { - *actual_len = ret; - conn->bytes_read += ret; - } else if (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { - return 0; // EOF - } else if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { - return 0; // Try again + if (buffer_append(&resp, buf, (size_t)ret) < 0) + goto cleanup_all; } - - return ret; + } while (ret > 0); + + // Parse status-line + headers with picohttpparser + struct phr_header hdrv[32]; + size_t hdr_count = sizeof(hdrv)/sizeof(hdrv[0]); + int minor, status; + const char *reason = NULL; + size_t reason_len = 0; + int pret = phr_parse_response( + resp.data, resp.size, + &minor, &status, + &reason, &reason_len, /* provide pointers, even if unused */ + hdrv, &hdr_count, + 0); + if (pret < 0) goto cleanup_all; + size_t header_len = (size_t)pret; + char *body_ptr = resp.data + header_len; + size_t body_len = resp.size - header_len; + + // Check for chunked encoding + int is_chunked = 0; + for (size_t i = 0; i < hdr_count; i++) { + if ((hdrv[i].name_len == 17 && + !strncasecmp(hdrv[i].name, "Transfer-Encoding", 17) && + memmem(hdrv[i].value, hdrv[i].value_len, "chunked", 7))) { + is_chunked = 1; + break; + } + } + + // If chunked, decode in-place + if (is_chunked) { + struct phr_chunked_decoder cd = {0}; + size_t blen = body_len; + ssize_t rest = phr_decode_chunked(&cd, body_ptr, &blen); + if (rest < 0) goto cleanup_all; + // blen = decoded length; rest = trailer start offset (ignored) + body_len = blen; + } + + // Return a Blob wrapping the body buffer (no extra copy) + JSValue blob = js_new_blob_stoned_copy(ctx, + (uint8_t *)body_ptr, body_len); + +cleanup_all: + buffer_free(&resp); + mbedtls_ssl_close_notify(&ssl); + +cleanup_tls: + mbedtls_net_free(&net_ctx); + mbedtls_ssl_free(&ssl); + mbedtls_ssl_config_free(&conf); + mbedtls_ctr_drbg_free(&drbg); + mbedtls_entropy_free(&entropy); + + JS_FreeCString(ctx, url); + free(host); free(port); free(path); + return blob; } -// JS function: fetch_start(url, options) - starts a chunked download -static JSValue js_fetch_start(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1 || !JS_IsString(argv[0])) { - return JS_ThrowTypeError(ctx, "fetch_start expects a URL string"); - } - - const char *url = JS_ToCString(ctx, argv[0]); - if (!url) { - return JS_ThrowTypeError(ctx, "Invalid URL"); - } - - char *host = NULL; - char *port = NULL; - char *path = NULL; - int use_ssl = 0; - buffer_t request_buf; - JSValue result = JS_EXCEPTION; - - buffer_init(&request_buf); - - // Parse URL - if (parse_url(url, &host, &port, &path, &use_ssl) < 0) { - JS_FreeCString(ctx, url); - return JS_ThrowTypeError(ctx, "Invalid URL format"); - } - - // Build request - buffer_append(&request_buf, "GET ", 4); - buffer_append(&request_buf, path, strlen(path)); - buffer_append(&request_buf, " HTTP/1.1\r\n", 11); - buffer_append(&request_buf, "Host: ", 6); - buffer_append(&request_buf, host, strlen(host)); - buffer_append(&request_buf, "\r\n", 2); - - // Add headers from options if provided - if (argc >= 2 && JS_IsObject(argv[1])) { - JSValue headers = JS_GetPropertyStr(ctx, argv[1], "headers"); - if (JS_IsObject(headers)) { - JSPropertyEnum *tab; - uint32_t len; - if (JS_GetOwnPropertyNames(ctx, &tab, &len, headers, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { - for (uint32_t i = 0; i < len; i++) { - JSValue key = JS_AtomToString(ctx, tab[i].atom); - JSValue val = JS_GetProperty(ctx, headers, tab[i].atom); - - const char *key_str = JS_ToCString(ctx, key); - const char *val_str = JS_ToCString(ctx, val); - - if (key_str && val_str) { - buffer_append(&request_buf, key_str, strlen(key_str)); - buffer_append(&request_buf, ": ", 2); - buffer_append(&request_buf, val_str, strlen(val_str)); - buffer_append(&request_buf, "\r\n", 2); - } - - JS_FreeCString(ctx, key_str); - JS_FreeCString(ctx, val_str); - JS_FreeValue(ctx, key); - JS_FreeValue(ctx, val); - } - js_free(ctx, tab); - } - } - JS_FreeValue(ctx, headers); - } - - buffer_append(&request_buf, "Connection: close\r\n\r\n", 21); - - // Create connection object - http_connection_t *conn = js_mallocz(ctx, sizeof(http_connection_t)); - if (!conn) { - result = JS_ThrowOutOfMemory(ctx); - goto cleanup; - } - - http_connection_init(conn); - - // Start connection - int ret = http_connection_start(conn, host, port, request_buf.data, request_buf.size, use_ssl); - if (ret != 0) { - char error_buf[256]; - mbedtls_strerror(ret, error_buf, sizeof(error_buf)); - http_connection_free(conn); - js_free(ctx, conn); - result = JS_ThrowInternalError(ctx, "Connection failed: %s", error_buf); - goto cleanup; - } - - // Create JS object to represent the connection - result = JS_NewObject(ctx); - JS_SetOpaque(result, conn); - - // Add properties - JS_DefinePropertyValueStr(ctx, result, "url", JS_NewString(ctx, url), JS_PROP_C_W_E); - JS_DefinePropertyValueStr(ctx, result, "_connection", JS_NewInt64(ctx, (intptr_t)conn), JS_PROP_C_W_E); - -cleanup: - JS_FreeCString(ctx, url); - free(host); - free(port); - free(path); - buffer_free(&request_buf); - - return result; -} - -// JS function: fetch_read_chunk(connection, maxBytes) - reads a chunk -static JSValue js_fetch_read_chunk(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "fetch_read_chunk expects a connection object"); - } - - JSValue conn_val = JS_GetPropertyStr(ctx, argv[0], "_connection"); - if (JS_IsUndefined(conn_val)) { - return JS_ThrowTypeError(ctx, "Invalid connection object"); - } - - int64_t conn_ptr; - if (JS_ToInt64(ctx, &conn_ptr, conn_val) < 0) { - JS_FreeValue(ctx, conn_val); - return JS_ThrowTypeError(ctx, "Invalid connection pointer"); - } - JS_FreeValue(ctx, conn_val); - - http_connection_t *conn = (http_connection_t *)(intptr_t)conn_ptr; - - size_t max_bytes = 8192; - if (argc >= 2) { - int64_t mb; - if (JS_ToInt64(ctx, &mb, argv[1]) == 0 && mb > 0) { - max_bytes = mb; - } - } - - uint8_t *buf = js_malloc(ctx, max_bytes); - if (!buf) { - return JS_ThrowOutOfMemory(ctx); - } - - size_t actual_len; - int ret = http_connection_read_chunk(conn, buf, max_bytes, &actual_len); - - JSValue result; - if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { - char error_buf[256]; - mbedtls_strerror(ret, error_buf, sizeof(error_buf)); - js_free(ctx, buf); - return JS_ThrowInternalError(ctx, "Read failed: %s", error_buf); - } - - if (actual_len == 0 && ret == 0) { - // EOF or would block - js_free(ctx, buf); - return JS_NULL; - } - - // Return chunk as ArrayBuffer - result = js_new_blob_stoned_copy(ctx, buf, actual_len); - js_free(ctx, buf); - - return result; -} - -// JS function: fetch_info(connection) - gets connection info -static JSValue js_fetch_info(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "fetch_info expects a connection object"); - } - - JSValue conn_val = JS_GetPropertyStr(ctx, argv[0], "_connection"); - if (JS_IsUndefined(conn_val)) { - return JS_ThrowTypeError(ctx, "Invalid connection object"); - } - - int64_t conn_ptr; - if (JS_ToInt64(ctx, &conn_ptr, conn_val) < 0) { - JS_FreeValue(ctx, conn_val); - return JS_ThrowTypeError(ctx, "Invalid connection pointer"); - } - JS_FreeValue(ctx, conn_val); - - http_connection_t *conn = (http_connection_t *)(intptr_t)conn_ptr; - - JSValue result = JS_NewObject(ctx); - JS_DefinePropertyValueStr(ctx, result, "headers_complete", JS_NewBool(ctx, conn->headers_complete), JS_PROP_C_W_E); - JS_DefinePropertyValueStr(ctx, result, "content_length", JS_NewInt64(ctx, conn->content_length), JS_PROP_C_W_E); - JS_DefinePropertyValueStr(ctx, result, "bytes_read", JS_NewInt64(ctx, conn->bytes_read), JS_PROP_C_W_E); - JS_DefinePropertyValueStr(ctx, result, "chunked_encoding", JS_NewBool(ctx, conn->chunked_encoding), JS_PROP_C_W_E); - - return result; -} - -// JS function: fetch_close(connection) - closes the connection -static JSValue js_fetch_close(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "fetch_close expects a connection object"); - } - - JSValue conn_val = JS_GetPropertyStr(ctx, argv[0], "_connection"); - if (JS_IsUndefined(conn_val)) { - return JS_ThrowTypeError(ctx, "Invalid connection object"); - } - - int64_t conn_ptr; - if (JS_ToInt64(ctx, &conn_ptr, conn_val) < 0) { - JS_FreeValue(ctx, conn_val); - return JS_ThrowTypeError(ctx, "Invalid connection pointer"); - } - JS_FreeValue(ctx, conn_val); - - http_connection_t *conn = (http_connection_t *)(intptr_t)conn_ptr; - http_connection_free(conn); - js_free(ctx, conn); - - // Clear the connection pointer - JS_DefinePropertyValueStr(ctx, argv[0], "_connection", JS_UNDEFINED, JS_PROP_C_W_E); - - return JS_UNDEFINED; -} - -// JS function: fetch(url, options) -static JSValue js_fetch(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1 || !JS_IsString(argv[0])) { - return JS_ThrowTypeError(ctx, "fetch expects a URL string"); - } - - const char *url = JS_ToCString(ctx, argv[0]); - if (!url) { - return JS_ThrowTypeError(ctx, "Invalid URL"); - } - - char *host = NULL; - char *port = NULL; - char *path = NULL; - int use_ssl = 0; - buffer_t request_buf; - buffer_t response_buf; - JSValue result = JS_EXCEPTION; - - buffer_init(&request_buf); - buffer_init(&response_buf); - - // Parse URL - if (parse_url(url, &host, &port, &path, &use_ssl) < 0) { - JS_FreeCString(ctx, url); - return JS_ThrowTypeError(ctx, "Invalid URL format"); - } - - // Build request - buffer_append(&request_buf, "GET ", 4); - buffer_append(&request_buf, path, strlen(path)); - buffer_append(&request_buf, " HTTP/1.1\r\n", 11); - buffer_append(&request_buf, "Host: ", 6); - buffer_append(&request_buf, host, strlen(host)); - buffer_append(&request_buf, "\r\n", 2); - - // Add headers from options if provided - if (argc >= 2 && JS_IsObject(argv[1])) { - JSValue headers = JS_GetPropertyStr(ctx, argv[1], "headers"); - if (JS_IsObject(headers)) { - JSPropertyEnum *tab; - uint32_t len; - if (JS_GetOwnPropertyNames(ctx, &tab, &len, headers, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { - for (uint32_t i = 0; i < len; i++) { - JSValue key = JS_AtomToString(ctx, tab[i].atom); - JSValue val = JS_GetProperty(ctx, headers, tab[i].atom); - - const char *key_str = JS_ToCString(ctx, key); - const char *val_str = JS_ToCString(ctx, val); - - if (key_str && val_str) { - buffer_append(&request_buf, key_str, strlen(key_str)); - buffer_append(&request_buf, ": ", 2); - buffer_append(&request_buf, val_str, strlen(val_str)); - buffer_append(&request_buf, "\r\n", 2); - } - - JS_FreeCString(ctx, key_str); - JS_FreeCString(ctx, val_str); - JS_FreeValue(ctx, key); - JS_FreeValue(ctx, val); - } - js_free(ctx, tab); - } - } - JS_FreeValue(ctx, headers); - } - - buffer_append(&request_buf, "Connection: close\r\n\r\n", 21); - - // Perform request - int ret; - if (use_ssl) { - ret = https_request(host, port, request_buf.data, request_buf.size, &response_buf); - } else { - ret = http_request(host, port, request_buf.data, request_buf.size, &response_buf); - } - - if (ret == 0 && response_buf.data) { - // Split headers / body - char *body = strstr(response_buf.data, "\r\n\r\n"); - if (!body) { - result = JS_ThrowInternalError(ctx, "Failed to parse HTTP response"); - goto cleanup; - } - size_t header_len = body + 4 - response_buf.data; - size_t body_len = response_buf.size - header_len; - body += 4; - - // Look for "Transfer-Encoding: chunked" - int is_chunked = (strstr(response_buf.data, "Transfer-Encoding: chunked") || - strstr(response_buf.data, "transfer-encoding: chunked")); - - printf("[HTTP] Total response size: %zu bytes\n", response_buf.size); - printf("[HTTP] Header length: %zu bytes\n", header_len); - printf("[HTTP] Body length before decoding: %zu bytes\n", body_len); - printf("[HTTP] Is chunked: %s\n", is_chunked ? "yes" : "no"); - - // Print first 100 chars of body before decoding - printf("[HTTP] First 100 chars of raw body: "); - for (size_t i = 0; i < 100 && i < body_len; i++) { - if (body[i] >= 32 && body[i] <= 126) { - printf("%c", body[i]); - } else { - printf("\\x%02x", (unsigned char)body[i]); - } - } - printf("\n"); - - buffer_t clean_body; - buffer_init(&clean_body); - - if (is_chunked) { - if (decode_chunked(&clean_body, body, body_len) < 0) { - buffer_free(&clean_body); - result = JS_ThrowInternalError(ctx, "Failed to decode chunked response"); - goto cleanup; - } - body = clean_body.data; - body_len = clean_body.size; - } else { - // not chunked → just copy - buffer_append(&clean_body, body, body_len); - body = clean_body.data; - body_len = clean_body.size; - } - - printf("[HTTP] Body length after decoding: %zu bytes\n", body_len); - printf("[HTTP] First 100 chars of decoded body: "); - for (size_t i = 0; i < 100 && i < body_len; i++) { - if (body[i] >= 32 && body[i] <= 126) { - printf("%c", body[i]); - } else { - printf("\\x%02x", (unsigned char)body[i]); - } - } - printf("\n"); - - // build blob from *body / body_len* exactly as before - result = js_new_blob_stoned_copy(ctx, (uint8_t *)body, body_len); - - buffer_free(&clean_body); - } else { - char error_buf[256]; - mbedtls_strerror(ret, error_buf, sizeof(error_buf)); - result = JS_ThrowInternalError(ctx, "Request failed: %s", error_buf); - } - -cleanup: - // Cleanup - JS_FreeCString(ctx, url); - free(host); - free(port); - free(path); - buffer_free(&request_buf); - buffer_free(&response_buf); - - return result; -} - -#define countof(a) (sizeof(a)/sizeof(*(a))) - -// Module exports +// Export the function as “fetch2” (for example) static const JSCFunctionListEntry js_http_funcs[] = { - JS_CFUNC_DEF("fetch", 2, js_fetch), - JS_CFUNC_DEF("fetch_start", 2, js_fetch_start), - JS_CFUNC_DEF("fetch_read_chunk", 2, js_fetch_read_chunk), - JS_CFUNC_DEF("fetch_info", 1, js_fetch_info), - JS_CFUNC_DEF("fetch_close", 1, js_fetch_close), + JS_CFUNC_DEF("fetch", 2, js_fetch_picoparser), }; -JSValue js_http_use(JSContext *js) -{ - JSValue exp = JS_NewObject(js); - JS_SetPropertyFunctionList(js, exp, js_http_funcs, countof(js_http_funcs)); - return exp; +JSValue js_http_use(JSContext *js) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyFunctionList(js, obj, js_http_funcs, + sizeof(js_http_funcs)/sizeof(js_http_funcs[0])); + return obj; } diff --git a/tests/blob.ce b/tests/blob.ce index 090b5f4a..a259b9cd 100644 --- a/tests/blob.ce +++ b/tests/blob.ce @@ -1,612 +1,387 @@ -// Blob test suite - +// blob_test.ce var Blob = use('blob'); -var time = use('time') +var os = use('os'); -// A small tolerance for floating comparisons if needed -var EPSILON = 1e-12; +log.console("=== Blob Module Test Suite ===\n"); -// Track test results -var testResults = { - type: 'test_results', - test_name: 'blob', - passed: 0, - failed: 0, - total: 0, - failures: [], - duration: 0 -}; +var passed = 0; +var failed = 0; -var startTime; - -function deepCompare(expected, actual, path) { - if (!path) path = ''; - - // Basic triple-equals check - if (expected === actual) { - return { passed: true, messages: [] }; - } - - // Compare booleans - if (typeof expected === 'boolean' && typeof actual === 'boolean') { - return { - passed: false, - messages: [ - 'Boolean mismatch at ' + path + ': expected ' + expected + ', got ' + actual - ] - }; - } - - // Compare numbers with tolerance - if (typeof expected === 'number' && typeof actual === 'number') { - if (isNaN(expected) && isNaN(actual)) { - return { passed: true, messages: [] }; +function test(name, fn) { + try { + fn(); + passed++; + log.console("✓ " + name); + } catch (e) { + failed++; + log.console("✗ " + name + ": " + e); } - var diff = Math.abs(expected - actual); - if (diff <= EPSILON) { - return { passed: true, messages: [] }; - } - return { - passed: false, - messages: [ - 'Number mismatch at ' + path + ': expected ' + expected + ', got ' + actual, - 'Difference of ' + diff + ' > EPSILON (' + EPSILON + ')' - ] - }; - } - - // Compare arrays - if (Array.isArray(expected) && Array.isArray(actual)) { - if (expected.length !== actual.length) { - return { - passed: false, - messages: [ - 'Array length mismatch at ' + path + ': expected len=' + expected.length + ', got len=' + actual.length - ] - }; - } - var messages = []; - for (var i = 0; i < expected.length; i++) { - var r = deepCompare(expected[i], actual[i], path + '[' + i + ']'); - if (!r.passed) messages.push.apply(messages, r.messages); - } - return { - passed: messages.length === 0, - messages: messages - }; - } - - // Compare objects - if ( - typeof expected === 'object' && - expected !== null && - typeof actual === 'object' && - actual !== null - ) { - var expKeys = Object.keys(expected).sort(); - var actKeys = Object.keys(actual).sort(); - if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) { - return { - passed: false, - messages: [ - 'Object keys mismatch at ' + path + ': expected [' + expKeys + '], got [' + actKeys + ']' - ] - }; - } - var messages = []; - for (var k = 0; k < expKeys.length; k++) { - var key = expKeys[k]; - var r = deepCompare(expected[key], actual[key], path ? path + '.' + key : key); - if (!r.passed) messages.push.apply(messages, r.messages); - } - return { passed: messages.length === 0, messages: messages }; - } - - // If none of the above, treat as a mismatch - return { - passed: false, - messages: [ - 'Mismatch at ' + path + ': expected ' + JSON.stringify(expected) + ', got ' + JSON.stringify(actual) - ] - }; } -// Helper to run a single test -function runTest(testName, testFn) { - var passed = true; - var messages = []; - - try { - var result = testFn(); - if (typeof result === 'object' && result !== null) { - passed = result.passed; - messages = result.messages || []; - } else { - // If the testFn returned a boolean or no return, interpret it - passed = !!result; +function assert(condition, message) { + if (!condition) { + throw new Error(message || "Assertion failed"); } - } catch (e) { - passed = false; - messages.push('Exception thrown: ' + (e.stack || e.toString())); - } - - // Update results - testResults.total++; - if (passed) { - testResults.passed++; - } else { - testResults.failed++; - testResults.failures.push({ - name: testName, - error: messages.join('\n') +} + +function assertEqual(actual, expected, message) { + if (actual !== expected) { + throw new Error(message || "Expected " + expected + ", got " + actual); + } +} + +// Test 1: Empty blob creation +test("Create empty blob", function() { + var b = new Blob(); + assertEqual(b.length, 0, "Empty blob should have length 0"); +}); + +// Test 2: Blob with capacity +test("Create blob with capacity", function() { + var b = new Blob(100); + assertEqual(b.length, 0, "New blob with capacity should still have length 0"); +}); + +// Test 3: Write and read single bit +test("Write and read single bit", function() { + var b = new Blob(); + b.write_bit(true); + b.write_bit(false); + b.write_bit(1); + b.write_bit(0); + assertEqual(b.length, 4, "Should have 4 bits after writing"); + + b.stone(); // Make it stone to read + assertEqual(b.read_logical(0), true, "First bit should be true"); + assertEqual(b.read_logical(1), false, "Second bit should be false"); + assertEqual(b.read_logical(2), true, "Third bit should be true (1)"); + assertEqual(b.read_logical(3), false, "Fourth bit should be false (0)"); +}); + +// Test 4: Out of range read returns null +test("Out of range read returns null", function() { + var b = new Blob(); + b.write_bit(true); + b.stone(); + assertEqual(b.read_logical(100), null, "Out of range read should return null"); + assertEqual(b.read_logical(-1), null, "Negative index read should return null"); +}); + +// Test 5: Write and read numbers +test("Write and read numbers", function() { + var b = new Blob(); + b.write_number(3.14159); + b.write_number(-42); + b.write_number(0); + b.write_number(1e20); + b.stone(); + + // Read back the numbers + assertEqual(b.read_number(0), 3.14159, "First number should match"); + assertEqual(b.read_number(64), -42, "Second number should match"); + assertEqual(b.read_number(128), 0, "Third number should match"); + assertEqual(b.read_number(192), 1e20, "Fourth number should match"); +}); + +// Test 6: Write and read text +test("Write and read text", function() { + var b = new Blob(); + b.write_text("Hello"); + b.write_text("World"); + b.write_text("🎉"); // Unicode test + b.stone(); + + assertEqual(b.read_text(0), "Hello", "First text should match"); + // Note: We need to know bit positions to read subsequent strings +}); + +// Test 7: Write and read blobs +test("Write and read blobs", function() { + var b1 = new Blob(); + b1.write_bit(true); + b1.write_bit(false); + b1.write_bit(true); + + var b2 = new Blob(10); // Give it some initial capacity + b2.write_blob(b1); + b2.write_bit(false); + assertEqual(b2.length, 4, "Combined blob should have 4 bits"); + + b2.stone(); + assertEqual(b2.read_logical(0), true); + assertEqual(b2.read_logical(1), false); + assertEqual(b2.read_logical(2), true); + assertEqual(b2.read_logical(3), false); +}); + +// Test 8: Copy constructor +test("Blob copy constructor", function() { + var b1 = new Blob(); + b1.write_bit(true); + b1.write_bit(false); + b1.write_bit(true); + b1.write_bit(true); + b1.stone(); + + var b2 = new Blob(b1); + b2.stone(); // Need to stone the copy before reading + assertEqual(b2.length, 4, "Copied blob should have same length"); + assertEqual(b2.read_logical(0), true); + assertEqual(b2.read_logical(3), true); +}); + +// Test 9: Partial copy constructor +test("Blob partial copy constructor", function() { + var b1 = new Blob(); + for (var i = 0; i < 10; i++) { + b1.write_bit(i % 2 === 0); + } + b1.stone(); + + var b2 = new Blob(b1, 2, 7); // Copy bits 2-6 (5 bits) + b2.stone(); // Need to stone the copy before reading + assertEqual(b2.length, 5, "Partial copy should have 5 bits"); + assertEqual(b2.read_logical(0), true); // bit 2 of original + assertEqual(b2.read_logical(2), true); // bit 4 of original +}); + +// Test 10: Blob with fill +test("Create blob with fill", function() { + var b1 = new Blob(8, true); // 8 bits all set to 1 + var b2 = new Blob(8, false); // 8 bits all set to 0 + + b1.stone(); + b2.stone(); + + for (var i = 0; i < 8; i++) { + assertEqual(b1.read_logical(i), true, "Bit " + i + " should be true"); + assertEqual(b2.read_logical(i), false, "Bit " + i + " should be false"); + } +}); + +// Test 11: Blob with random function +test("Create blob with random function", function() { + var sequence = [true, false, true, true, false]; + var index = 0; + + var b = new Blob(5, function() { + return sequence[index++] ? 1 : 0; }); - } - - // Log individual result - log.console(testName + ' - ' + (passed ? 'Passed' : 'Failed')); - if (!passed && messages.length > 0) { - log.console(' ' + messages.join('\n ')); - } -} - -// Test suite -var tests = [ - // 1) Ensure we can create a blank blob - { - name: "new Blob() should produce an empty antestone blob of length 0", - run: function() { - var b = new Blob(); - var length = b.length; - var passed = (b instanceof Blob && length === 0); - var messages = []; - if (!(b instanceof Blob)) messages.push("Returned object is not recognized as a blob"); - if (length !== 0) messages.push('Expected length 0, got ' + length); - return { passed: passed, messages: messages }; - } - }, - - // 2) Make a blob with some capacity - { - name: "new Blob(16) should create a blob with capacity >=16 bits and length=0", - run: function() { - var b = new Blob(16); - var isBlob = b instanceof Blob; - var length = b.length; - var passed = isBlob && length === 0; - var messages = []; - if (!isBlob) messages.push("Not recognized as a blob"); - if (length !== 0) messages.push('Expected length=0, got ' + length); - return { passed: passed, messages: messages }; - } - }, - - // 3) Make a blob with (length, logical) - but can't read until stone - { - name: "new Blob(5, true) should create a blob of length=5 bits, all 1s - needs stone to read", - run: function() { - var b = new Blob(5, true); - var len = b.length; - if (len !== 5) { - return { - passed: false, - messages: ['Expected length=5, got ' + len] - }; - } - - // Try to read before stone - should return null - var bitVal = b.read_logical(0); - if (bitVal !== null) { - return { - passed: false, - messages: ['Expected null when reading antestone blob, got ' + bitVal] - }; - } - - // Stone it - stone(b); - - // Now check bits - for (var i = 0; i < 5; i++) { - bitVal = b.read_logical(i); - if (bitVal !== true) { - return { - passed: false, - messages: ['Bit at index ' + i + ' expected true, got ' + bitVal] - }; - } - } - return { passed: true, messages: [] }; - } - }, - - // 4) Write bits to an empty blob, then stone and read - { - name: "write_bit() on an empty blob, then stone and read_logical() to verify bits", - run: function() { - var b = new Blob(); // starts length=0 - // write bits: true, false, true - b.write_bit(true); // bit #0 - b.write_bit(false); // bit #1 - b.write_bit(true); // bit #2 - var len = b.length; - if (len !== 3) { - return { - passed: false, - messages: ['Expected length=3, got ' + len] - }; - } - - // Must stone before reading - stone(b); - - var bits = [ - b.read_logical(0), - b.read_logical(1), - b.read_logical(2) - ]; - var compare = deepCompare([true, false, true], bits); - return compare; - } - }, - - // 5) Stone a blob, then attempt to write -> fail - { - name: "Stoning a blob should prevent further writes", - run: function() { - var b = new Blob(5, false); - // Stone it - stone(b); - // Try to write - var passed = true; - var messages = []; - try { - b.write_bit(true); - passed = false; - messages.push("Expected an error or refusal when writing to a stone blob, but none occurred"); - } catch (e) { - // We expect an exception or some error scenario - } - return { passed: passed, messages: messages }; - } - }, - - // 6) make(blob, from, to) - copying range from an existing blob - { - name: "new Blob(existing_blob, from, to) can copy partial range", - run: function() { - // Create a 10-bit blob: pattern T F T F T F T F T F - var original = new Blob(); - for (var i = 0; i < 10; i++) { - original.write_bit(i % 2 === 0); - } - // Copy bits [2..7) - var copy = new Blob(original, 2, 7); - var len = copy.length; - if (len !== 5) { - return { - passed: false, - messages: ['Expected length=5, got ' + len] - }; - } - - // Stone the copy to read from it - stone(copy); - - var bits = []; - for (var i = 0; i < len; i++) { - bits.push(copy.read_logical(i)); - } - var compare = deepCompare([true, false, true, false, true], bits); - return compare; - } - }, - - // 7) Checking instanceof - { - name: "instanceof should correctly identify blob vs. non-blob", - run: function() { - var b = new Blob(); - var isB = b instanceof Blob; - var isNum = 42 instanceof Blob; - var isObj = { length: 3 } instanceof Blob; - var passed = (isB === true && isNum === false && isObj === false); - var messages = []; - if (!passed) { - messages.push('Expected (b instanceof Blob)=true, (42 instanceof Blob)=false, ({} instanceof Blob)=false; got ' + isB + ', ' + isNum + ', ' + isObj); - } - return { passed: passed, messages: messages }; - } - }, - - // 8) Test write_blob - { - name: "write_blob() should append one blob to another", - run: function() { - var b1 = new Blob(); - b1.write_bit(true); - b1.write_bit(false); - - var b2 = new Blob(); - b2.write_bit(true); - b2.write_bit(true); - - b1.write_blob(b2); - - if (b1.length !== 4) { - return { - passed: false, - messages: ['Expected length=4 after write_blob, got ' + b1.length] - }; - } - - stone(b1); - var bits = []; - for (var i = 0; i < 4; i++) { - bits.push(b1.read_logical(i)); - } - - return deepCompare([true, false, true, true], bits); - } - }, - - // 9) Test write_fit and read_fit - { - name: "write_fit() and read_fit() should handle fixed-size bit fields", - run: function() { - var b = new Blob(); - b.write_fit(5, 3); // Write value 5 in 3 bits (101) - b.write_fit(7, 4); // Write value 7 in 4 bits (0111) - - if (b.length !== 7) { - return { - passed: false, - messages: ['Expected length=7, got ' + b.length] - }; - } - - stone(b); - - var val1 = b.read_fit(0, 3); - var val2 = b.read_fit(3, 4); - - if (val1 !== 5 || val2 !== 7) { - return { - passed: false, - messages: ['Expected read_fit to return 5 and 7, got ' + val1 + ' and ' + val2] - }; - } - - return { passed: true, messages: [] }; - } - }, - - // 10) Test write_kim and read_kim - SKIPPED due to native error - { - name: "write_kim() and read_kim() should handle kim encoding", - run: function() { - // Skip this test as it's causing native errors - return { passed: true, messages: ['Test skipped due to native implementation issues'] }; - } - }, - - // 11) Test write_text and read_text - { - name: "write_text() and read_text() should handle text encoding", - run: function() { - var b = new Blob(); - b.write_text("Hello"); - - stone(b); - - var result = b.read_text(0); - - if (result.text !== "Hello") { - return { - passed: false, - messages: ['Expected text "Hello", got "' + result.text + '"'] - }; - } - - return { passed: true, messages: [] }; - } - }, - - // 12) Test write_dec64 and read_dec64 - { - name: "write_dec64() and read_dec64() should handle decimal encoding", - run: function() { - var b = new Blob(); - b.write_dec64(3.14159); - b.write_dec64(-42.5); - - stone(b); - - var val1 = b.read_dec64(0); - var val2 = b.read_dec64(64); - - // Allow small floating point differences - var diff1 = Math.abs(val1 - 3.14159); - var diff2 = Math.abs(val2 - (-42.5)); - - if (diff1 > EPSILON || diff2 > EPSILON) { - return { - passed: false, - messages: ['Expected dec64 values 3.14159 and -42.5, got ' + val1 + ' and ' + val2] - }; - } - - return { passed: true, messages: [] }; - } - }, - - // 13) Test write_pad and pad? - { - name: "write_pad() and pad?() should handle block padding", - run: function() { - var b = new Blob(); - b.write_bit(true); - b.write_bit(false); - b.write_bit(true); - // Length is now 3 - b.write_pad(8); // Pad to multiple of 8 - - if (b.length !== 8) { - return { - passed: false, - messages: ['Expected length=8 after padding, got ' + b.length] - }; - } - - stone(b); - - // Check pad? function - var isPadded = b["pad?"](3, 8); - if (!isPadded) { - return { - passed: false, - messages: ['Expected pad?(3, 8) to return true'] - }; - } - - // Verify padding pattern: original bits, then 1, then 0s - var bits = []; - for (var i = 0; i < 8; i++) { - bits.push(b.read_logical(i)); - } - - return deepCompare([true, false, true, true, false, false, false, false], bits); - } - }, - - // 14) Test Blob.kim_length static function - { - name: "Blob.kim_length() should calculate correct kim encoding lengths", - run: function() { - var len1 = Blob.kim_length(42); // Should be 8 bits - var len2 = Blob.kim_length(1000); // Should be 16 bits - var len3 = Blob.kim_length("Hello"); // 8 bits for length + 8*5 for chars = 48 - - if (len1 !== 8) { - return { - passed: false, - messages: ['Expected kim_length(42)=8, got ' + len1] - }; - } - - if (len2 !== 16) { - return { - passed: false, - messages: ['Expected kim_length(1000)=16, got ' + len2] - }; - } - - if (len3 !== 48) { - return { - passed: false, - messages: ['Expected kim_length("Hello")=48, got ' + len3] - }; - } - - return { passed: true, messages: [] }; - } - }, - - // 15) Test write_bit with numeric 0 and 1 - { - name: "write_bit() should accept 0, 1, true, false", - run: function() { - var b = new Blob(); - b.write_bit(1); - b.write_bit(0); - b.write_bit(true); - b.write_bit(false); - - stone(b); - - var bits = []; - for (var i = 0; i < 4; i++) { - bits.push(b.read_logical(i)); - } - - return deepCompare([true, false, true, false], bits); - } - }, - - // 16) Test read_blob to create copies - { - name: "read_blob() should create partial copies of stone blobs", - run: function() { - var b = new Blob(); - for (var i = 0; i < 10; i++) { - b.write_bit(i % 3 === 0); // Pattern: T,F,F,T,F,F,T,F,F,T - } - stone(b); - - var copy = b.read_blob(3, 7); // Extract bits 3-6 - stone(copy); - - if (copy.length !== 4) { - return { - passed: false, - messages: ['Expected copy length=4, got ' + copy.length] - }; - } - - var bits = []; - for (var i = 0; i < 4; i++) { - bits.push(copy.read_logical(i)); - } - - // Bits 3-6 from original: T,F,F,T - return deepCompare([true, false, false, true], bits); - } - }, - - // 17) Test random blob creation - { - name: "new Blob(length, random_func) should create random blob", - run: function() { - // Simple random function that alternates - var counter = 0; - var randomFunc = function() { return counter++; }; - - var b = new Blob(8, randomFunc); - stone(b); - - if (b.length !== 8) { - return { - passed: false, - messages: ['Expected length=8, got ' + b.length] - }; - } - - // Check pattern matches counter LSB: 0,1,0,1,0,1,0,1 - var bits = []; - for (var i = 0; i < 8; i++) { - bits.push(b.read_logical(i)); - } - - return deepCompare([false, true, false, true, false, true, false, true], bits); - } - } -]; - -// Message receiver -$_.receiver(function(msg) { - if (msg.type === 'run_tests') { - log.console("HERE") - startTime = time.number(); - // Run all tests - for (var i = 0; i < tests.length; i++) { - var test = tests[i]; - runTest(test.name, test.run); + b.stone(); + for (var i = 0; i < 5; i++) { + assertEqual(b.read_logical(i), sequence[i], "Bit " + i + " should match sequence"); + } +}); + +// Test 12: Write pad and pad check +test("Write pad and check padding", function() { + var b = new Blob(); + b.write_bit(true); + b.write_bit(false); + b.write_bit(true); + b.write_pad(8); // Pad to 8-bit boundary + + assertEqual(b.length, 8, "Should be padded to 8 bits"); + b.stone(); + + assert(b['pad?'](3, 8), "Should detect valid padding at position 3"); + assert(!b['pad?'](2, 8), "Should detect invalid padding at position 2"); +}); + +// Test 13: read_blob method +test("Read blob from stone blob", function() { + var b1 = new Blob(); + for (var i = 0; i < 16; i++) { + b1.write_bit(i % 3 === 0); + } + b1.stone(); + + var b2 = b1.read_blob(4, 12); // Read bits 4-11 (8 bits) + b2.stone(); // Need to stone the read blob before reading from it + assertEqual(b2.length, 8, "Read blob should have 8 bits"); + + // Check the pattern + assertEqual(b2.read_logical(2), true); // Original bit 6 (6 % 3 === 0) + assertEqual(b2.read_logical(5), true); // Original bit 9 (9 % 3 === 0) +}); + +// Test 14: Stone immutability +test("Stone blob is immutable", function() { + var b = new Blob(); + b.write_bit(true); + b.stone(); + + var threw = false; + try { + b.write_bit(false); // Should fail on stone blob + } catch (e) { + threw = true; + } + assert(threw, "Writing to stone blob should throw error"); +}); + +// Test 15: Multiple stone calls +test("Multiple stone calls are safe", function() { + var b = new Blob(); + b.write_bit(true); + b.stone(); + b.stone(); // Should be safe to call again + assertEqual(b.read_logical(0), true, "Blob data should remain intact"); +}); + +// Test 16: Invalid constructor arguments +test("Invalid constructor arguments", function() { + var threw = false; + try { + var b = new Blob("invalid"); + } catch (e) { + threw = true; + } + assert(threw, "Invalid constructor arguments should throw"); +}); + +// Test 17: Write invalid bit values +test("Write bit validation", function() { + var b = new Blob(); + b.write_bit(0); + b.write_bit(1); + + var threw = false; + try { + b.write_bit(2); // Should only accept 0, 1, true, false + } catch (e) { + threw = true; + } + assert(threw, "write_bit with value 2 should throw"); +}); + +// Test 18: Complex data round-trip +test("Complex data round-trip", function() { + var b = new Blob(); + + // Write mixed data + b.write_text("Test"); + b.write_number(123.456); + b.write_bit(true); + b.write_bit(false); + b.write_number(-999.999); + + var originalLength = b.length; + b.stone(); + + // Verify we can create a copy + var b2 = new Blob(b); + b2.stone(); // Need to stone the copy before reading + assertEqual(b2.length, originalLength, "Copy should have same length"); + assertEqual(b2.read_text(0), "Test", "First text should match"); +}); + +// Test 19: Zero capacity blob +test("Zero capacity blob", function() { + var b = new Blob(0); + assertEqual(b.length, 0, "Zero capacity blob should have length 0"); + b.write_bit(true); // Should auto-expand + assertEqual(b.length, 1, "Should expand when writing"); +}); + +// Test 20: Large blob handling +test("Large blob handling", function() { + var b = new Blob(); + var testSize = 1000; + + // Write many bits + for (var i = 0; i < testSize; i++) { + b.write_bit(i % 7 === 0); } - // Calculate duration - testResults.duration = time.number() - startTime; + assertEqual(b.length, testSize, "Should have " + testSize + " bits"); + b.stone(); - // Send results back - send(msg, testResults); - } -}); \ No newline at end of file + // Verify pattern + assertEqual(b.read_logical(0), true, "Bit 0 should be true"); + assertEqual(b.read_logical(7), true, "Bit 7 should be true"); + assertEqual(b.read_logical(14), true, "Bit 14 should be true"); + assertEqual(b.read_logical(15), false, "Bit 15 should be false"); +}); + +// Test 21: Non-stone blob read methods should throw +test("Non-stone blob read methods should throw", function() { + var b = new Blob(); + b.write_bit(true); + b.write_number(42); + b.write_text("test"); + + // Try to read without stoning - should throw + var threw = false; + try { + b.read_number(0); + } catch (e) { + threw = true; + } + assert(threw, "read_number on non-stone blob should throw"); + + threw = false; + try { + b.read_text(0); + } catch (e) { + threw = true; + } + assert(threw, "read_text on non-stone blob should throw"); + + threw = false; + try { + b.read_blob(0, 10); + } catch (e) { + threw = true; + } + assert(threw, "read_blob on non-stone blob should throw"); + + threw = false; + try { + b['pad?'](0, 8); + } catch (e) { + threw = true; + } + assert(threw, "pad? on non-stone blob should throw"); +}); + +// Test 22: Empty text write and read +test("Empty text write and read", function() { + var b = new Blob(); + b.write_text(""); + b.stone(); + assertEqual(b.read_text(0), "", "Empty string should round-trip"); +}); + +// Test 23: Blob error on invalid read positions +test("Invalid read positions", function() { + var b = new Blob(); + b.write_number(42); + b.stone(); + + var threw = false; + try { + b.read_number(-10); // Negative position + } catch (e) { + threw = true; + } + assert(threw, "Negative position should throw"); + + threw = false; + try { + b.read_number(1000); // Beyond blob length + } catch (e) { + threw = true; + } + assert(threw, "Position beyond length should throw"); +}); + +// Print summary +log.console("\n=== Test Summary ==="); +log.console("Total tests: " + (passed + failed)); +log.console("Passed: " + passed); +log.console("Failed: " + failed); +log.console("\nOverall: " + (failed === 0 ? "PASSED" : "FAILED")); + +os.exit(failed === 0 ? 0 : 1); \ No newline at end of file diff --git a/tests/http.ce b/tests/http.ce index 170f6986..b4b5bd8b 100644 --- a/tests/http.ce +++ b/tests/http.ce @@ -2,9 +2,6 @@ var http = use('http'); var os = use('os'); -var getter = http.fetch('tortoise.png') -var file = getter.data() // invokes the data stream to wait for it - var got = false var count = 0 http.fetch("https://dictionary.ink/find?word=theological", { @@ -19,10 +16,6 @@ http.fetch("https://dictionary.ink/find?word=theological", { } }) -while (!got) { - http.poll() -} - log.console(`got hit ${count} times`) os.exit() diff --git a/tests/modules.ce b/tests/modules.ce index 1bfabd8c..e426702a 100644 --- a/tests/modules.ce +++ b/tests/modules.ce @@ -19,7 +19,7 @@ baz = "qux@2.0" [arrays] items = ["one", "two", "three"] ` -var parsed = toml.parse(test_toml) +var parsed = toml.decode(test_toml) log.console("Parsed module: " + parsed.module) log.console("Dependencies: " + json.encode(parsed.dependencies)) log.console("✓ TOML parser working")