aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-08-06 23:28:13 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-08-06 23:28:13 -0700
commit4b1f89114e02f8472b63794534698ec13929080e (patch)
tree8e038d24c11a8d4b471cb72fc86de135ec3042e5
parent6e4da63abeb76540bfefa9efcb3d823f8b50eb61 (diff)
downloadbun-4b1f89114e02f8472b63794534698ec13929080e.tar.gz
bun-4b1f89114e02f8472b63794534698ec13929080e.tar.zst
bun-4b1f89114e02f8472b63794534698ec13929080e.zip
Query String parser with JS integration
Former-commit-id: 8542778c30e9757fa87514f46ff5086d7c8f6bfa
-rw-r--r--demos/css-stress-test/framework.tsx1
-rw-r--r--src/javascript/jsc/api/router.zig55
-rw-r--r--src/javascript/jsc/bindings/bindings.cpp103
-rw-r--r--src/javascript/jsc/bindings/bindings.zig39
-rw-r--r--src/javascript/jsc/bindings/headers-cpp.h2
-rw-r--r--src/javascript/jsc/bindings/headers.h6
-rw-r--r--src/javascript/jsc/bindings/headers.zig4
-rw-r--r--src/query_string_map.zig652
-rw-r--r--src/string_immutable.zig12
9 files changed, 866 insertions, 8 deletions
diff --git a/demos/css-stress-test/framework.tsx b/demos/css-stress-test/framework.tsx
index 47fa068cc..22b1a3109 100644
--- a/demos/css-stress-test/framework.tsx
+++ b/demos/css-stress-test/framework.tsx
@@ -2,6 +2,7 @@ import ReactDOMServer from "react-dom/server.browser";
addEventListener("fetch", async (event: FetchEvent) => {
var route = Wundle.match(event);
+ console.log("Route", JSON.stringify(route.query, null, 2));
const { default: PageComponent } = await import(route.filepath);
// const router = Wundle.Router.match(event);
// console.log("Route", router.name);
diff --git a/src/javascript/jsc/api/router.zig b/src/javascript/jsc/api/router.zig
index 6e6fea896..f1c892d94 100644
--- a/src/javascript/jsc/api/router.zig
+++ b/src/javascript/jsc/api/router.zig
@@ -4,6 +4,7 @@ const Api = @import("../../../api/schema.zig").Api;
const FilesystemRouter = @import("../../../router.zig");
const http = @import("../../../http.zig");
const JavaScript = @import("../javascript.zig");
+const QueryStringMap = @import("../../../query_string_map.zig").QueryStringMap;
usingnamespace @import("../bindings/bindings.zig");
usingnamespace @import("../webcore/response.zig");
const Router = @This();
@@ -11,8 +12,7 @@ const Router = @This();
const VirtualMachine = JavaScript.VirtualMachine;
route: *const FilesystemRouter.Match,
-file_path_str: js.JSStringRef = null,
-pathname_str: js.JSStringRef = null,
+query_string_map: ?QueryStringMap = null,
pub fn importRoute(
this: *Router,
@@ -231,7 +231,9 @@ pub fn getFilePath(
pub fn finalize(
this: *Router,
) void {
- // this.deinit();
+ if (this.query_string_map) |*map| {
+ map.deinit();
+ }
}
pub fn getPathname(
@@ -283,6 +285,40 @@ pub fn getKind(
return KindEnum.init(this.route.name).toValue(VirtualMachine.vm.global).asRef();
}
+threadlocal var query_string_values_buf: [256]string = undefined;
+threadlocal var query_string_value_refs_buf: [256]ZigString = undefined;
+pub fn createQueryObject(ctx: js.JSContextRef, map: *QueryStringMap, exception: js.ExceptionRef) callconv(.C) js.JSValueRef {
+ const QueryObjectCreator = struct {
+ query: *QueryStringMap,
+ pub fn create(this: *@This(), obj: *JSObject, global: *JSGlobalObject) void {
+ var iter = this.query.iter();
+ var str: ZigString = undefined;
+ while (iter.next(&query_string_values_buf)) |entry| {
+ str = ZigString.init(entry.name);
+
+ std.debug.assert(entry.values.len > 0);
+ if (entry.values.len > 1) {
+ var values = query_string_value_refs_buf[0..entry.values.len];
+ for (entry.values) |value, i| {
+ values[i] = ZigString.init(value);
+ }
+ obj.putRecord(VirtualMachine.vm.global, &str, values.ptr, values.len);
+ } else {
+ query_string_value_refs_buf[0] = ZigString.init(entry.values[0]);
+
+ obj.putRecord(VirtualMachine.vm.global, &str, &query_string_value_refs_buf, 1);
+ }
+ }
+ }
+ };
+
+ var creator = QueryObjectCreator{ .query = map };
+
+ var value = JSObject.createWithInitializer(QueryObjectCreator, &creator, VirtualMachine.vm.global, map.getNameCount());
+
+ return value.asRef();
+}
+
pub fn getQuery(
this: *Router,
ctx: js.JSContextRef,
@@ -290,5 +326,16 @@ pub fn getQuery(
prop: js.JSStringRef,
exception: js.ExceptionRef,
) js.JSValueRef {
- return ZigString.init(this.route.query_string).toValue(VirtualMachine.vm.global).asRef();
+ if (this.query_string_map == null) {
+ if (QueryStringMap.init(getAllocator(ctx), this.route.query_string)) |map| {
+ this.query_string_map = map;
+ } else |err| {}
+ }
+
+ // If it's still null, the query string has no names.
+ if (this.query_string_map) |*map| {
+ return createQueryObject(ctx, map, exception);
+ } else {
+ return JSValue.createEmptyObject(VirtualMachine.vm.global, 0).asRef();
+ }
}
diff --git a/src/javascript/jsc/bindings/bindings.cpp b/src/javascript/jsc/bindings/bindings.cpp
index 4d8d13483..c66b5c94b 100644
--- a/src/javascript/jsc/bindings/bindings.cpp
+++ b/src/javascript/jsc/bindings/bindings.cpp
@@ -21,6 +21,7 @@
#include <JavaScriptCore/JSObject.h>
#include <JavaScriptCore/JSSet.h>
#include <JavaScriptCore/JSString.h>
+#include <JavaScriptCore/ObjectConstructor.h>
#include <JavaScriptCore/ParserError.h>
#include <JavaScriptCore/ScriptExecutable.h>
#include <JavaScriptCore/StackFrame.h>
@@ -31,9 +32,109 @@
#include <wtf/text/StringCommon.h>
#include <wtf/text/StringImpl.h>
#include <wtf/text/StringView.h>
-
#include <wtf/text/WTFString.h>
extern "C" {
+JSC__JSValue
+JSC__JSObject__create(JSC__JSGlobalObject *globalObject, size_t initialCapacity, void *arg2,
+ void (*ArgFn3)(void *arg0, JSC__JSObject *arg1, JSC__JSGlobalObject *arg2)) {
+ JSC::JSObject *object =
+ JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), initialCapacity);
+
+ ArgFn3(arg2, object, globalObject);
+
+ return JSC::JSValue::encode(object);
+}
+
+JSC__JSValue JSC__JSValue__createEmptyObject(JSC__JSGlobalObject *globalObject,
+ size_t initialCapacity) {
+ return JSC::JSValue::encode(
+ JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), initialCapacity));
+}
+
+void JSC__JSObject__putRecord(JSC__JSObject *object, JSC__JSGlobalObject *global, ZigString *key,
+ ZigString *values, size_t valuesLen) {
+ auto scope = DECLARE_THROW_SCOPE(global->vm());
+ auto ident = Zig::toIdentifier(*key, global);
+ JSC::PropertyDescriptor descriptor;
+
+ descriptor.setEnumerable(1);
+ descriptor.setConfigurable(1);
+ descriptor.setWritable(1);
+
+ if (valuesLen == 1) {
+ descriptor.setValue(JSC::jsString(global->vm(), Zig::toString(values[0])));
+ } else {
+
+ JSC::JSArray *array = nullptr;
+ {
+ JSC::ObjectInitializationScope initializationScope(global->vm());
+ if ((array = JSC::JSArray::tryCreateUninitializedRestricted(
+ initializationScope, nullptr,
+ global->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
+ valuesLen))) {
+
+ for (size_t i = 0; i < valuesLen; ++i) {
+ array->initializeIndexWithoutBarrier(
+ initializationScope, i, JSC::jsString(global->vm(), Zig::toString(values[i])));
+ }
+ }
+ }
+
+ if (!array) {
+ JSC::throwOutOfMemoryError(global, scope);
+ return;
+ }
+
+ descriptor.setValue(array);
+ }
+
+ object->methodTable(global->vm())->defineOwnProperty(object, global, ident, descriptor, true);
+ object->putDirect(global->vm(), ident, descriptor.value());
+ scope.release();
+}
+void JSC__JSValue__putRecord(JSC__JSValue objectValue, JSC__JSGlobalObject *global, ZigString *key,
+ ZigString *values, size_t valuesLen) {
+ JSC::JSValue objValue = JSC::JSValue::decode(objectValue);
+ JSC::JSObject *object = objValue.asCell()->getObject();
+ auto scope = DECLARE_THROW_SCOPE(global->vm());
+ auto ident = Zig::toIdentifier(*key, global);
+ JSC::PropertyDescriptor descriptor;
+
+ descriptor.setEnumerable(1);
+ descriptor.setConfigurable(1);
+ descriptor.setWritable(1);
+
+ if (valuesLen == 1) {
+ descriptor.setValue(JSC::jsString(global->vm(), Zig::toString(values[0])));
+ } else {
+
+ JSC::JSArray *array = nullptr;
+ {
+ JSC::ObjectInitializationScope initializationScope(global->vm());
+ if ((array = JSC::JSArray::tryCreateUninitializedRestricted(
+ initializationScope, nullptr,
+ global->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
+ valuesLen))) {
+
+ for (size_t i = 0; i < valuesLen; ++i) {
+ array->initializeIndexWithoutBarrier(
+ initializationScope, i, JSC::jsString(global->vm(), Zig::toString(values[i])));
+ }
+ }
+ }
+
+ if (!array) {
+ JSC::throwOutOfMemoryError(global, scope);
+ return;
+ }
+
+ descriptor.setValue(array);
+ }
+
+ object->methodTable(global->vm())->defineOwnProperty(object, global, ident, descriptor, true);
+ object->putDirect(global->vm(), ident, descriptor.value());
+ scope.release();
+}
// This is very naive!
JSC__JSInternalPromise *JSC__VM__reloadModule(JSC__VM *vm, JSC__JSGlobalObject *arg1,
diff --git a/src/javascript/jsc/bindings/bindings.zig b/src/javascript/jsc/bindings/bindings.zig
index 9373a00a5..e2b2679ae 100644
--- a/src/javascript/jsc/bindings/bindings.zig
+++ b/src/javascript/jsc/bindings/bindings.zig
@@ -18,6 +18,29 @@ pub const JSObject = extern struct {
});
}
+ const InitializeCallback = fn (ctx: ?*c_void, obj: [*c]JSObject, global: [*c]JSGlobalObject) callconv(.C) void;
+ pub fn create(global_object: *JSGlobalObject, length: usize, ctx: *c_void, initializer: InitializeCallback) JSValue {
+ return cppFn("create", .{
+ global_object,
+ length,
+ ctx,
+ initializer,
+ });
+ }
+
+ pub fn Initializer(comptime Ctx: type, comptime func: fn (*Ctx, obj: *JSObject, global: *JSGlobalObject) void) type {
+ return struct {
+ pub fn call(this: ?*c_void, obj: [*c]JSObject, global: [*c]JSGlobalObject) callconv(.C) void {
+ @call(.{ .modifier = .always_inline }, func, .{ @ptrCast(*Ctx, @alignCast(@alignOf(*Ctx), this.?)), obj.?, global.? });
+ }
+ };
+ }
+
+ pub fn createWithInitializer(comptime Ctx: type, creator: *Ctx, global: *JSGlobalObject, length: usize) JSValue {
+ const Type = Initializer(Ctx, Ctx.create);
+ return create(global, length, creator, Type.call);
+ }
+
pub fn getIndex(this: *JSObject, globalThis: *JSGlobalObject, i: u32) JSValue {
return cppFn("getIndex", .{
this,
@@ -26,6 +49,10 @@ pub const JSObject = extern struct {
});
}
+ pub fn putRecord(this: *JSObject, global: *JSGlobalObject, key: *ZigString, values: [*]ZigString, values_len: usize) void {
+ return cppFn("putRecord", .{ this, global, key, values, values_len });
+ }
+
pub fn getDirect(this: *JSObject, globalThis: *JSGlobalObject, str: ZigString) JSValue {
return cppFn("getDirect", .{
this,
@@ -44,6 +71,8 @@ pub const JSObject = extern struct {
}
pub const Extern = [_][]const u8{
+ "putRecord",
+ "create",
"getArrayLength",
"getIndex",
"putAtIndex",
@@ -1126,6 +1155,14 @@ pub const JSValue = enum(i64) {
return @intToEnum(JSValue, @intCast(i64, @ptrToInt(ptr)));
}
+ pub fn createEmptyObject(global: *JSGlobalObject, len: usize) JSValue {
+ return cppFn("createEmptyObject", .{ global, len });
+ }
+
+ pub fn putRecord(value: JSValue, global: *JSGlobalObject, key: *ZigString, values: [*]ZigString, values_len: usize) void {
+ return cppFn("putRecord", .{ value, global, key, values, values_len });
+ }
+
pub fn getErrorsProperty(this: JSValue, globalObject: *JSGlobalObject) JSValue {
return cppFn("getErrorsProperty", .{ this, globalObject });
}
@@ -1363,7 +1400,7 @@ pub const JSValue = enum(i64) {
return @intToPtr(*c_void, @intCast(usize, @enumToInt(this)));
}
- pub const Extern = [_][]const u8{ "asPromise", "isClass", "getNameProperty", "getClassName", "getErrorsProperty", "toInt32", "toBoolean", "isInt32", "isIterable", "forEach", "isAggregateError", "toZigException", "isException", "toWTFString", "hasProperty", "getPropertyNames", "getDirect", "putDirect", "get", "getIfExists", "asString", "asObject", "asNumber", "isError", "jsNull", "jsUndefined", "jsTDZValue", "jsBoolean", "jsDoubleNumber", "jsNumberFromDouble", "jsNumberFromChar", "jsNumberFromU16", "jsNumberFromInt32", "jsNumberFromInt64", "jsNumberFromUint64", "isUndefined", "isNull", "isUndefinedOrNull", "isBoolean", "isAnyInt", "isUInt32AsAnyInt", "isInt32AsAnyInt", "isNumber", "isString", "isBigInt", "isHeapBigInt", "isBigInt32", "isSymbol", "isPrimitive", "isGetterSetter", "isCustomGetterSetter", "isObject", "isCell", "asCell", "toString", "toStringOrNull", "toPropertyKey", "toPropertyKeyValue", "toObject", "toString", "getPrototype", "getPropertyByPropertyName", "eqlValue", "eqlCell", "isCallable" };
+ pub const Extern = [_][]const u8{ "createEmptyObject", "putRecord", "asPromise", "isClass", "getNameProperty", "getClassName", "getErrorsProperty", "toInt32", "toBoolean", "isInt32", "isIterable", "forEach", "isAggregateError", "toZigException", "isException", "toWTFString", "hasProperty", "getPropertyNames", "getDirect", "putDirect", "get", "getIfExists", "asString", "asObject", "asNumber", "isError", "jsNull", "jsUndefined", "jsTDZValue", "jsBoolean", "jsDoubleNumber", "jsNumberFromDouble", "jsNumberFromChar", "jsNumberFromU16", "jsNumberFromInt32", "jsNumberFromInt64", "jsNumberFromUint64", "isUndefined", "isNull", "isUndefinedOrNull", "isBoolean", "isAnyInt", "isUInt32AsAnyInt", "isInt32AsAnyInt", "isNumber", "isString", "isBigInt", "isHeapBigInt", "isBigInt32", "isSymbol", "isPrimitive", "isGetterSetter", "isCustomGetterSetter", "isObject", "isCell", "asCell", "toString", "toStringOrNull", "toPropertyKey", "toPropertyKeyValue", "toObject", "toString", "getPrototype", "getPropertyByPropertyName", "eqlValue", "eqlCell", "isCallable" };
};
pub const PropertyName = extern struct {
diff --git a/src/javascript/jsc/bindings/headers-cpp.h b/src/javascript/jsc/bindings/headers-cpp.h
index 1e578bd0a..e23e9dd84 100644
--- a/src/javascript/jsc/bindings/headers-cpp.h
+++ b/src/javascript/jsc/bindings/headers-cpp.h
@@ -1,4 +1,4 @@
-//-- AUTOGENERATED FILE -- 1628213116
+//-- AUTOGENERATED FILE -- 1628316335
// clang-format off
#pragma once
diff --git a/src/javascript/jsc/bindings/headers.h b/src/javascript/jsc/bindings/headers.h
index 813a01750..abafa1279 100644
--- a/src/javascript/jsc/bindings/headers.h
+++ b/src/javascript/jsc/bindings/headers.h
@@ -1,4 +1,4 @@
-//-- AUTOGENERATED FILE -- 1628213116
+//-- AUTOGENERATED FILE -- 1628316335
// clang-format: off
#pragma once
@@ -231,10 +231,12 @@ typedef void* JSClassRef;
#pragma mark - JSC::JSObject
+CPP_DECL JSC__JSValue JSC__JSObject__create(JSC__JSGlobalObject* arg0, size_t arg1, void* arg2, void (* ArgFn3)(void* arg0, JSC__JSObject* arg1, JSC__JSGlobalObject* arg2));
CPP_DECL size_t JSC__JSObject__getArrayLength(JSC__JSObject* arg0);
CPP_DECL JSC__JSValue JSC__JSObject__getDirect(JSC__JSObject* arg0, JSC__JSGlobalObject* arg1, ZigString arg2);
CPP_DECL JSC__JSValue JSC__JSObject__getIndex(JSC__JSObject* arg0, JSC__JSGlobalObject* arg1, uint32_t arg2);
CPP_DECL void JSC__JSObject__putDirect(JSC__JSObject* arg0, JSC__JSGlobalObject* arg1, ZigString arg2, JSC__JSValue JSValue3);
+CPP_DECL void JSC__JSObject__putRecord(JSC__JSObject* arg0, JSC__JSGlobalObject* arg1, ZigString* arg2, ZigString* arg3, size_t arg4);
CPP_DECL JSC__JSValue ZigString__toErrorInstance(const ZigString* arg0, JSC__JSGlobalObject* arg1);
CPP_DECL JSC__JSValue ZigString__toValue(ZigString arg0, JSC__JSGlobalObject* arg1);
@@ -409,6 +411,7 @@ CPP_DECL JSC__JSCell* JSC__JSValue__asCell(JSC__JSValue JSValue0);
CPP_DECL double JSC__JSValue__asNumber(JSC__JSValue JSValue0);
CPP_DECL bJSC__JSObject JSC__JSValue__asObject(JSC__JSValue JSValue0);
CPP_DECL JSC__JSString* JSC__JSValue__asString(JSC__JSValue JSValue0);
+CPP_DECL JSC__JSValue JSC__JSValue__createEmptyObject(JSC__JSGlobalObject* arg0, size_t arg1);
CPP_DECL bool JSC__JSValue__eqlCell(JSC__JSValue JSValue0, JSC__JSCell* arg1);
CPP_DECL bool JSC__JSValue__eqlValue(JSC__JSValue JSValue0, JSC__JSValue JSValue1);
CPP_DECL void JSC__JSValue__forEach(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, void (* ArgFn2)(JSC__VM* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2));
@@ -452,6 +455,7 @@ CPP_DECL JSC__JSValue JSC__JSValue__jsNumberFromU16(uint16_t arg0);
CPP_DECL JSC__JSValue JSC__JSValue__jsNumberFromUint64(uint64_t arg0);
CPP_DECL JSC__JSValue JSC__JSValue__jsTDZValue();
CPP_DECL JSC__JSValue JSC__JSValue__jsUndefined();
+CPP_DECL void JSC__JSValue__putRecord(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, ZigString* arg2, ZigString* arg3, size_t arg4);
CPP_DECL bool JSC__JSValue__toBoolean(JSC__JSValue JSValue0);
CPP_DECL int32_t JSC__JSValue__toInt32(JSC__JSValue JSValue0);
CPP_DECL JSC__JSObject* JSC__JSValue__toObject(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1);
diff --git a/src/javascript/jsc/bindings/headers.zig b/src/javascript/jsc/bindings/headers.zig
index 901422733..de854fe53 100644
--- a/src/javascript/jsc/bindings/headers.zig
+++ b/src/javascript/jsc/bindings/headers.zig
@@ -97,10 +97,12 @@ pub const JSC__ObjectPrototype = struct_JSC__ObjectPrototype;
pub const JSC__CallFrame = bJSC__CallFrame;
pub const JSC__MapIteratorPrototype = struct_JSC__MapIteratorPrototype;
+pub extern fn JSC__JSObject__create(arg0: [*c]JSC__JSGlobalObject, arg1: usize, arg2: ?*c_void, ArgFn3: ?fn (?*c_void, [*c]JSC__JSObject, [*c]JSC__JSGlobalObject) callconv(.C) void) JSC__JSValue;
pub extern fn JSC__JSObject__getArrayLength(arg0: [*c]JSC__JSObject) usize;
pub extern fn JSC__JSObject__getDirect(arg0: [*c]JSC__JSObject, arg1: [*c]JSC__JSGlobalObject, arg2: ZigString) JSC__JSValue;
pub extern fn JSC__JSObject__getIndex(arg0: [*c]JSC__JSObject, arg1: [*c]JSC__JSGlobalObject, arg2: u32) JSC__JSValue;
pub extern fn JSC__JSObject__putDirect(arg0: [*c]JSC__JSObject, arg1: [*c]JSC__JSGlobalObject, arg2: ZigString, JSValue3: JSC__JSValue) void;
+pub extern fn JSC__JSObject__putRecord(arg0: [*c]JSC__JSObject, arg1: [*c]JSC__JSGlobalObject, arg2: [*c]ZigString, arg3: [*c]ZigString, arg4: usize) void;
pub extern fn ZigString__toErrorInstance(arg0: [*c]const ZigString, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue;
pub extern fn ZigString__toValue(arg0: ZigString, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue;
pub extern fn JSC__JSCell__getObject(arg0: [*c]JSC__JSCell) [*c]JSC__JSObject;
@@ -233,6 +235,7 @@ pub extern fn JSC__JSValue__asCell(JSValue0: JSC__JSValue) [*c]JSC__JSCell;
pub extern fn JSC__JSValue__asNumber(JSValue0: JSC__JSValue) f64;
pub extern fn JSC__JSValue__asObject(JSValue0: JSC__JSValue) bJSC__JSObject;
pub extern fn JSC__JSValue__asString(JSValue0: JSC__JSValue) [*c]JSC__JSString;
+pub extern fn JSC__JSValue__createEmptyObject(arg0: [*c]JSC__JSGlobalObject, arg1: usize) JSC__JSValue;
pub extern fn JSC__JSValue__eqlCell(JSValue0: JSC__JSValue, arg1: [*c]JSC__JSCell) bool;
pub extern fn JSC__JSValue__eqlValue(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) bool;
pub extern fn JSC__JSValue__forEach(JSValue0: JSC__JSValue, arg1: [*c]JSC__JSGlobalObject, ArgFn2: ?fn ([*c]JSC__VM, [*c]JSC__JSGlobalObject, JSC__JSValue) callconv(.C) void) void;
@@ -276,6 +279,7 @@ pub extern fn JSC__JSValue__jsNumberFromU16(arg0: u16) JSC__JSValue;
pub extern fn JSC__JSValue__jsNumberFromUint64(arg0: u64) JSC__JSValue;
pub extern fn JSC__JSValue__jsTDZValue(...) JSC__JSValue;
pub extern fn JSC__JSValue__jsUndefined(...) JSC__JSValue;
+pub extern fn JSC__JSValue__putRecord(JSValue0: JSC__JSValue, arg1: [*c]JSC__JSGlobalObject, arg2: [*c]ZigString, arg3: [*c]ZigString, arg4: usize) void;
pub extern fn JSC__JSValue__toBoolean(JSValue0: JSC__JSValue) bool;
pub extern fn JSC__JSValue__toInt32(JSValue0: JSC__JSValue) i32;
pub extern fn JSC__JSValue__toObject(JSValue0: JSC__JSValue, arg1: [*c]JSC__JSGlobalObject) [*c]JSC__JSObject;
diff --git a/src/query_string_map.zig b/src/query_string_map.zig
new file mode 100644
index 000000000..aec41ac98
--- /dev/null
+++ b/src/query_string_map.zig
@@ -0,0 +1,652 @@
+const std = @import("std");
+const Api = @import("./api/schema.zig").Api;
+usingnamespace @import("./global.zig");
+
+/// QueryString hash table that does few allocations and preserves the original order
+pub const QueryStringMap = struct {
+ allocator: *std.mem.Allocator,
+ slice: string,
+ buffer: []u8,
+ list: Param.List,
+ name_count: ?usize = null,
+
+ threadlocal var _name_count: [8]string = undefined;
+ pub fn getNameCount(this: *QueryStringMap) usize {
+ if (this.name_count == null) {
+ var count: usize = 0;
+ var iterate = this.iter();
+ while (iterate.next(&_name_count) != null) {
+ count += 1;
+ }
+ this.name_count = count;
+ }
+ return this.name_count.?;
+ }
+
+ pub fn iter(this: *const QueryStringMap) Iterator {
+ return Iterator.init(this);
+ }
+
+ pub const Iterator = struct {
+ // Assume no query string param will exceed 2048 keys
+ const VisitedMap = std.bit_set.ArrayBitSet(usize, 2048);
+
+ i: usize = 0,
+ map: *const QueryStringMap,
+ visited: VisitedMap,
+
+ const Result = struct {
+ name: string,
+ values: []string,
+ };
+
+ pub fn init(map: *const QueryStringMap) Iterator {
+ return Iterator{ .i = 0, .map = map, .visited = VisitedMap.initEmpty() };
+ }
+
+ pub fn next(this: *Iterator, target: []string) ?Result {
+ while (this.visited.isSet(this.i)) : (this.i += 1) {}
+ if (this.i >= this.map.list.len) return null;
+
+ var count: usize = 0;
+ var slice = this.map.list.slice();
+ const hash = slice.items(.name_hash)[this.i];
+ var result = Result{ .name = this.map.str(slice.items(.name)[this.i]), .values = target[0..1] };
+ target[0] = this.map.str(slice.items(.value)[this.i]);
+
+ this.visited.set(this.i);
+ this.i += 1;
+
+ var remainder_hashes = slice.items(.name_hash)[this.i..];
+ var remainder_values = slice.items(.value)[this.i..];
+
+ var target_i: usize = 1;
+ var current_i: usize = 0;
+
+ while (std.mem.indexOfScalar(u64, remainder_hashes[current_i..], hash)) |next_index| {
+ const real_i = current_i + next_index + this.i;
+ if (comptime isDebug) {
+ std.debug.assert(!this.visited.isSet(real_i));
+ }
+
+ this.visited.set(real_i);
+ target[target_i] = this.map.str(remainder_values[current_i + next_index]);
+ target_i += 1;
+ result.values = target[0..target_i];
+
+ current_i += next_index + 1;
+ if (target_i >= target.len) return result;
+ if (real_i + 1 >= this.map.list.len) return result;
+ }
+
+ return result;
+ }
+ };
+
+ pub fn str(this: *const QueryStringMap, ptr: Api.StringPointer) string {
+ return this.slice[ptr.offset .. ptr.offset + ptr.length];
+ }
+
+ pub fn getIndex(this: *const QueryStringMap, input: string) ?usize {
+ const hash = std.hash.Wyhash.hash(0, input);
+ return std.mem.indexOfScalar(u64, this.list.items(.name_hash), hash);
+ }
+
+ pub fn get(this: *const QueryStringMap, input: string) ?string {
+ const hash = std.hash.Wyhash.hash(0, input);
+ const _slice = this.list.slice();
+ const i = std.mem.indexOfScalar(u64, _slice.items(.name_hash), hash) orelse return null;
+ return this.str(_slice.items(.value)[i]);
+ }
+
+ pub fn has(this: *const QueryStringMap, input: string) bool {
+ return this.getIndex(input) != null;
+ }
+
+ pub fn getAll(this: *const QueryStringMap, input: string, target: []string) usize {
+ const hash = std.hash.Wyhash.hash(0, input);
+ const _slice = this.list.slice();
+ return @call(.{ .modifier = .always_inline }, getAllWithHashFromOffset, .{ this, target, hash, 0, _slice });
+ }
+
+ pub fn getAllWithHashFromOffset(this: *const QueryStringMap, target: []string, hash: u64, offset: usize, _slice: Param.List.Slice) usize {
+ var remainder_hashes = _slice.items(.name_hash)[offset..];
+ var remainder_values = _slice.items(.value)[offset..];
+ var target_i: usize = 0;
+ while (remainder_hashes.len > 0 and target_i < target.len) {
+ const i = std.mem.indexOfScalar(u64, remainder_hashes, hash) orelse break;
+ target[target_i] = this.str(remainder_values[i]);
+ remainder_values = remainder_values[i + 1 ..];
+ remainder_hashes = remainder_hashes[i + 1 ..];
+ target_i += 1;
+ }
+ return target_i;
+ }
+
+ pub const Param = struct {
+ name: Api.StringPointer,
+ name_hash: u64,
+ value: Api.StringPointer,
+
+ pub const List = std.MultiArrayList(Param);
+ };
+
+ pub fn init(
+ allocator: *std.mem.Allocator,
+ query_string: string,
+ ) !?QueryStringMap {
+ var list = Param.List{};
+
+ var scanner = Scanner.init(query_string);
+ var count: usize = 0;
+ var estimated_str_len: usize = 0;
+
+ var nothing_needs_decoding = true;
+ while (scanner.next()) |result| {
+ if (result.name_needs_decoding or result.value_needs_decoding) {
+ nothing_needs_decoding = false;
+ }
+ estimated_str_len += result.name.length + result.value.length;
+ count += 1;
+ }
+
+ if (count == 0) return null;
+
+ scanner = Scanner.init(query_string);
+ try list.ensureTotalCapacity(allocator, count);
+
+ if (nothing_needs_decoding) {
+ scanner = Scanner.init(query_string);
+ while (scanner.next()) |result| {
+ std.debug.assert(!result.name_needs_decoding);
+ std.debug.assert(!result.value_needs_decoding);
+
+ var name = result.name;
+ var value = result.value;
+ const name_hash: u64 = std.hash.Wyhash.hash(0, result.rawName(query_string));
+ list.appendAssumeCapacity(Param{ .name = name, .value = value, .name_hash = name_hash });
+ }
+
+ return QueryStringMap{
+ .list = list,
+ .buffer = &[_]u8{},
+ .slice = query_string,
+ .allocator = allocator,
+ };
+ }
+
+ var buf = try std.ArrayList(u8).initCapacity(allocator, estimated_str_len);
+ var writer = buf.writer();
+ var buf_writer_pos: u32 = 0;
+
+ var list_slice = list.slice();
+ const Writer = @TypeOf(writer);
+ while (scanner.next()) |result| {
+ var name = result.name;
+ var value = result.value;
+ var name_hash: u64 = undefined;
+ if (result.name_needs_decoding) {
+ name.length = PercentEncoding.decode(Writer, writer, query_string[name.offset..][0..name.length]) catch continue;
+ name.offset = buf_writer_pos;
+ buf_writer_pos += name.length;
+ } else {
+ name_hash = std.hash.Wyhash.hash(0, result.rawName(query_string));
+ if (std.mem.indexOfScalar(u64, list_slice.items(.name_hash), name_hash)) |index| {
+ name = list_slice.items(.name)[index];
+ } else {
+ name.length = PercentEncoding.decode(Writer, writer, query_string[name.offset..][0..name.length]) catch continue;
+ name.offset = buf_writer_pos;
+ buf_writer_pos += name.length;
+ }
+ }
+
+ value.length = PercentEncoding.decode(Writer, writer, query_string[value.offset..][0..value.length]) catch continue;
+ value.offset = buf_writer_pos;
+ buf_writer_pos += value.length;
+
+ list.appendAssumeCapacity(Param{ .name = name, .value = value, .name_hash = name_hash });
+ }
+
+ buf.expandToCapacity();
+ return QueryStringMap{
+ .list = list,
+ .buffer = buf.items,
+ .slice = buf.items[0..buf_writer_pos],
+ .allocator = allocator,
+ };
+ }
+
+ pub fn deinit(this: *QueryStringMap) void {
+ if (this.buffer.len > 0) {
+ this.allocator.free(this.buffer);
+ }
+
+ if (this.list.len > 0) {
+ this.list.deinit(this.allocator);
+ }
+ }
+};
+
+pub const PercentEncoding = struct {
+ pub fn decode(comptime Writer: type, writer: Writer, input: string) !u32 {
+ var i: usize = 0;
+ var written: u32 = 0;
+ // unlike JavaScript's decodeURIComponent, we are not handling invalid surrogate pairs
+ // we are assuming the input is valid ascii
+ while (i < input.len) {
+ switch (input[i]) {
+ '%' => {
+ if (!(i + 3 <= input.len and strings.isASCIIHexDigit(input[i + 1]) and strings.isASCIIHexDigit(input[i + 2]))) return error.DecodingError;
+ try writer.writeByte((strings.toASCIIHexValue(input[i + 1]) << 4) | strings.toASCIIHexValue(input[i + 2]));
+ i += 3;
+ written += 1;
+ continue;
+ },
+ else => {
+ const start = i;
+ i += 1;
+
+ // scan ahead assuming .writeAll is faster than .writeByte one at a time
+ while (i < input.len and input[i] != '%') : (i += 1) {}
+ try writer.writeAll(input[start..i]);
+ written += @truncate(u32, i - start);
+ },
+ }
+ }
+
+ return written;
+ }
+};
+
+pub const Scanner = struct {
+ query_string: string,
+ i: usize,
+
+ pub fn init(query_string: string) Scanner {
+ if (query_string.len > 0 and query_string[0] == '?') {
+ return Scanner{ .query_string = query_string, .i = 1 };
+ }
+
+ return Scanner{ .query_string = query_string, .i = 0 };
+ }
+
+ pub const Result = struct {
+ name_needs_decoding: bool = false,
+ value_needs_decoding: bool = false,
+ name: Api.StringPointer,
+ value: Api.StringPointer,
+
+ pub inline fn rawName(this: *const Result, query_string: string) string {
+ return if (this.name.length > 0) query_string[this.name.offset..][0..this.name.length] else "";
+ }
+
+ pub inline fn rawValue(this: *const Result, query_string: string) string {
+ return if (this.value.length > 0) query_string[this.value.offset..][0..this.value.length] else "";
+ }
+ };
+
+ /// Get the next query string parameter without allocating memory.
+ pub fn next(this: *Scanner) ?Result {
+ var relative_i: usize = 0;
+ defer this.i += relative_i;
+
+ // reuse stack space
+ // otherwise we'd recursively call the function
+ loop: while (true) {
+ if (this.i >= this.query_string.len) return null;
+
+ var slice = this.query_string[this.i..];
+ relative_i = 0;
+ var name = Api.StringPointer{ .offset = @truncate(u32, this.i), .length = 0 };
+ var value = Api.StringPointer{ .offset = 0, .length = 0 };
+ var name_needs_decoding = false;
+
+ while (relative_i < slice.len) {
+ const char = slice[relative_i];
+ switch (char) {
+ '=' => {
+ name.length = @truncate(u32, relative_i);
+ relative_i += 1;
+
+ value.offset = @truncate(u32, relative_i + this.i);
+
+ const offset = relative_i;
+ var value_needs_decoding = false;
+ while (relative_i < slice.len and slice[relative_i] != '&') : (relative_i += 1) {
+ value_needs_decoding = value_needs_decoding or switch (slice[relative_i]) {
+ '%', '+' => true,
+ else => false,
+ };
+ }
+ value.length = @truncate(u32, relative_i - offset);
+ // If the name is empty and it's just a value, skip it.
+ // This is kind of an opinion. But, it's hard to see where that might be intentional.
+ if (name.length == 0) return null;
+ return Result{ .name = name, .value = value, .name_needs_decoding = name_needs_decoding, .value_needs_decoding = value_needs_decoding };
+ },
+ '%', '+' => {
+ name_needs_decoding = true;
+ },
+ '&' => {
+ // key&
+ if (relative_i > 0) {
+ name.length = @truncate(u32, relative_i);
+ return Result{ .name = name, .value = value, .name_needs_decoding = name_needs_decoding, .value_needs_decoding = false };
+ }
+
+ // &&&&&&&&&&&&&key=value
+ while (relative_i < slice.len and slice[relative_i] == '&') : (relative_i += 1) {}
+ this.i += relative_i;
+
+ // reuse stack space
+ continue :loop;
+ },
+ else => {},
+ }
+
+ relative_i += 1;
+ }
+
+ if (relative_i == 0) {
+ return null;
+ }
+
+ name.length = @truncate(u32, relative_i);
+ return Result{ .name = name, .value = value, .name_needs_decoding = name_needs_decoding };
+ }
+ }
+};
+
+const expect = std.testing.expect;
+const expectString = std.testing.expectEqualStrings;
+test "Scanner.init" {
+ var scanner = Scanner.init("?hello=true");
+ try expect(scanner.i == 1);
+ scanner = Scanner.init("hello=true");
+ try expect(scanner.i == 0);
+}
+
+test "Scanner.next" {
+ var scanner = Scanner.init("?hello=true&welcome=to&the=what&is=this&1=100&&&&bacon&&&&what=true&ok&=100");
+ var result: Scanner.Result = undefined;
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding == false);
+ try expect(result.value_needs_decoding == false);
+ try expectString(result.rawName(scanner.query_string), "hello");
+ try expectString(result.rawValue(scanner.query_string), "true");
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding == false);
+ try expect(result.value_needs_decoding == false);
+ try expectString(result.rawName(scanner.query_string), "welcome");
+ try expectString(result.rawValue(scanner.query_string), "to");
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding == false);
+ try expect(result.value_needs_decoding == false);
+ try expectString(result.rawName(scanner.query_string), "the");
+ try expectString(result.rawValue(scanner.query_string), "what");
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding == false);
+ try expect(result.value_needs_decoding == false);
+ try expectString(result.rawName(scanner.query_string), "is");
+ try expectString(result.rawValue(scanner.query_string), "this");
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding == false);
+ try expect(result.value_needs_decoding == false);
+ try expectString(result.rawName(scanner.query_string), "1");
+ try expectString(result.rawValue(scanner.query_string), "100");
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding == false);
+ try expect(result.value_needs_decoding == false);
+ try expectString(result.rawName(scanner.query_string), "bacon");
+ try expectString(result.rawValue(scanner.query_string), "");
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding == false);
+ try expect(result.value_needs_decoding == false);
+ try expectString(result.rawName(scanner.query_string), "what");
+ try expectString(result.rawValue(scanner.query_string), "true");
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding == false);
+ try expect(result.value_needs_decoding == false);
+ try expectString(result.rawName(scanner.query_string), "ok");
+ try expectString(result.rawValue(scanner.query_string), "");
+ try expect(scanner.next() == null);
+}
+
+test "Scanner.next - % encoded" {
+ var scanner = Scanner.init("?foo%20=%201023%20&%20what%20the%20fuck%20=%20am%20i%20looking%20at");
+ var result: Scanner.Result = undefined;
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding);
+ try expect(result.value_needs_decoding);
+ try expectString(result.rawName(scanner.query_string), "foo%20");
+ try expectString(result.rawValue(scanner.query_string), "%201023%20");
+ result = scanner.next() orelse return try std.testing.expect(false);
+ try expect(result.name_needs_decoding);
+ try expect(result.value_needs_decoding);
+ try expectString(result.rawName(scanner.query_string), "%20what%20the%20fuck%20");
+ try expectString(result.rawValue(scanner.query_string), "%20am%20i%20looking%20at");
+ try expect(scanner.next() == null);
+}
+
+test "PercentEncoding.decode" {
+ var buffer: [4096]u8 = undefined;
+ std.mem.set(u8, &buffer, 0);
+
+ var stream = std.io.fixedBufferStream(&buffer);
+ var writer = stream.writer();
+ const Writer = @TypeOf(writer);
+
+ {
+ const written = try PercentEncoding.decode(Writer, writer, "hello%20world%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B%20%2B");
+ const correct = "hello world + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +";
+ try expect(written == correct.len);
+ try expectString(buffer[0..written], correct);
+ }
+
+ stream.reset();
+
+ {
+ const correct = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ const written = try PercentEncoding.decode(Writer, writer, correct);
+ try expect(written == correct.len);
+ try expectString(buffer[0..written], correct);
+ }
+
+ stream.reset();
+
+ {
+ const correct = "hello my name is ?????";
+ const input = "hello%20my%20name%20is%20%3F%3F%3F%3F%3F";
+ const written = try PercentEncoding.decode(Writer, writer, correct);
+ try expect(written == correct.len);
+ try expectString(buffer[0..written], correct);
+ }
+}
+
+test "QueryStringMap (full)" {
+
+ // This is copy pasted from a random twitter thread on Chrome
+ const url = "?cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_quote_count=true&include_reply_count=1&tweet_mode=extended&dm_users=false&include_groups=true&include_inbox_timelines=true&include_ext_media_color=true&supports_reactions=true&muting_enabled=false&nsfw_filtering_enabled=false&cursor=GRwmkMCq6fLUnMAnFpDAquny1JzAJyUAAAA&filter_low_quality=true&include_quality=all&ext=mediaColor&ext=altText&ext=mediaStats&ext=highlightedLabel&ext=voiceInfo";
+ // from chrome's devtools
+ const fixture = .{
+ .@"cards_platform" = "Web-12",
+ .@"include_cards" = "1",
+ .@"include_ext_alt_text" = "true",
+ .@"include_quote_count" = "true",
+ .@"include_reply_count" = "1",
+ .@"tweet_mode" = "extended",
+ .@"dm_users" = "false",
+ .@"include_groups" = "true",
+ .@"include_inbox_timelines" = "true",
+ .@"include_ext_media_color" = "true",
+ .@"supports_reactions" = "true",
+ .@"muting_enabled" = "false",
+ .@"nsfw_filtering_enabled" = "false",
+ .@"cursor" = "GRwmkMCq6fLUnMAnFpDAquny1JzAJyUAAAA",
+ .@"filter_low_quality" = "true",
+ .@"include_quality" = "all",
+ .@"ext" = &[_]string{ "mediaColor", "altText", "mediaStats", "highlightedLabel", "voiceInfo" },
+ };
+
+ var map = (try QueryStringMap.init(std.testing.allocator, url)) orelse return try std.testing.expect(false);
+ defer map.deinit();
+ try expectString(fixture.cards_platform, map.get("cards_platform").?);
+ try expectString(fixture.include_cards, map.get("include_cards").?);
+ try expectString(fixture.include_ext_alt_text, map.get("include_ext_alt_text").?);
+ try expectString(fixture.include_quote_count, map.get("include_quote_count").?);
+ try expectString(fixture.include_reply_count, map.get("include_reply_count").?);
+ try expectString(fixture.tweet_mode, map.get("tweet_mode").?);
+ try expectString(fixture.dm_users, map.get("dm_users").?);
+ try expectString(fixture.include_groups, map.get("include_groups").?);
+ try expectString(fixture.include_inbox_timelines, map.get("include_inbox_timelines").?);
+ try expectString(fixture.include_ext_media_color, map.get("include_ext_media_color").?);
+ try expectString(fixture.supports_reactions, map.get("supports_reactions").?);
+ try expectString(fixture.muting_enabled, map.get("muting_enabled").?);
+ try expectString(fixture.nsfw_filtering_enabled, map.get("nsfw_filtering_enabled").?);
+ try expectString(fixture.cursor, map.get("cursor").?);
+ try expectString(fixture.filter_low_quality, map.get("filter_low_quality").?);
+ try expectString(fixture.include_quality, map.get("include_quality").?);
+ try expectString(fixture.ext[0], map.get("ext").?);
+
+ var target: [fixture.ext.len]string = undefined;
+ try expect((map.getAll("ext", &target)) == fixture.ext.len);
+
+ for (target) |item, i| {
+ try expectString(
+ fixture.ext[i],
+ item,
+ );
+ }
+}
+
+test "QueryStringMap not encoded" {
+ const url = "?hey=1&wow=true";
+ const fixture = .{
+ .@"hey" = "1",
+ .@"wow" = "true",
+ };
+ const url_slice = std.mem.span(url);
+ var map = (try QueryStringMap.init(std.testing.allocator, url_slice)) orelse return try std.testing.expect(false);
+ try expect(map.buffer.len == 0);
+ try expect(url_slice.ptr == map.slice.ptr);
+ defer map.deinit();
+ try expectString(fixture.hey, map.get("hey").?);
+ try expectString(fixture.wow, map.get("wow").?);
+}
+const expectEqual = std.testing.expectEqual;
+test "QueryStringMap Iterator" {
+ // This is copy pasted from a random twitter thread on Chrome
+ // The only difference from the one above is "ext" is moved before the last one
+ // This is to test order of iteration
+ const url = "?cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_quote_count=true&include_reply_count=1&tweet_mode=extended&dm_users=false&include_groups=true&include_inbox_timelines=true&include_ext_media_color=true&supports_reactions=true&muting_enabled=false&nsfw_filtering_enabled=false&cursor=GRwmkMCq6fLUnMAnFpDAquny1JzAJyUAAAA&filter_low_quality=true&ext=voiceInfo&include_quality=all&ext=mediaColor&ext=altText&ext=mediaStats&ext=highlightedLabel";
+ // from chrome's devtools
+ const fixture = .{
+ .@"cards_platform" = "Web-12",
+ .@"include_cards" = "1",
+ .@"include_ext_alt_text" = "true",
+ .@"include_quote_count" = "true",
+ .@"include_reply_count" = "1",
+ .@"tweet_mode" = "extended",
+ .@"dm_users" = "false",
+ .@"include_groups" = "true",
+ .@"include_inbox_timelines" = "true",
+ .@"include_ext_media_color" = "true",
+ .@"supports_reactions" = "true",
+ .@"muting_enabled" = "false",
+ .@"nsfw_filtering_enabled" = "false",
+ .@"cursor" = "GRwmkMCq6fLUnMAnFpDAquny1JzAJyUAAAA",
+ .@"filter_low_quality" = "true",
+ .@"include_quality" = "all",
+ .@"ext" = &[_]string{
+ "voiceInfo",
+ "mediaColor",
+ "altText",
+ "mediaStats",
+ "highlightedLabel",
+ },
+ };
+
+ var map = (try QueryStringMap.init(std.testing.allocator, url)) orelse return try std.testing.expect(false);
+ defer map.deinit();
+ var buf_: [48]string = undefined;
+ var buf = std.mem.span(&buf_);
+ var iter = map.iter();
+
+ var result: QueryStringMap.Iterator.Result = iter.next(buf) orelse return try expect(false);
+ try expectString("cards_platform", result.name);
+ try expectString(fixture.cards_platform, result.values[0]);
+ try expectEqual(result.values.len, 1);
+
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("include_cards", result.name);
+ try expectString(fixture.include_cards, result.values[0]);
+ try expectEqual(result.values.len, 1);
+
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("include_ext_alt_text", result.name);
+ try expectString(fixture.include_ext_alt_text, result.values[0]);
+ try expectEqual(result.values.len, 1);
+
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("include_quote_count", result.name);
+ try expectString(fixture.include_quote_count, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("include_reply_count", result.name);
+ try expectString(fixture.include_reply_count, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("tweet_mode", result.name);
+ try expectString(fixture.tweet_mode, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("dm_users", result.name);
+ try expectString(fixture.dm_users, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("include_groups", result.name);
+ try expectString(fixture.include_groups, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("include_inbox_timelines", result.name);
+ try expectString(fixture.include_inbox_timelines, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("include_ext_media_color", result.name);
+ try expectString(fixture.include_ext_media_color, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("supports_reactions", result.name);
+ try expectString(fixture.supports_reactions, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("muting_enabled", result.name);
+ try expectString(fixture.muting_enabled, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("nsfw_filtering_enabled", result.name);
+ try expectString(fixture.nsfw_filtering_enabled, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("cursor", result.name);
+ try expectString(fixture.cursor, result.values[0]);
+ try expectEqual(result.values.len, 1);
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("filter_low_quality", result.name);
+ try expectString(fixture.filter_low_quality, result.values[0]);
+ try expectEqual(result.values.len, 1);
+
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("ext", result.name);
+ try expectEqual(result.values.len, fixture.ext.len);
+ for (fixture.ext) |ext, i| {
+ try expectString(ext, result.values[i]);
+ }
+
+ result = iter.next(buf) orelse return try expect(false);
+ try expectString("include_quality", result.name);
+ try expectString(fixture.include_quality, result.values[0]);
+ try expectEqual(result.values.len, 1);
+
+ try expect(iter.next(buf) == null);
+}
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index d5f41c82e..7330798b7 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -444,6 +444,18 @@ pub fn sortDesc(in: []string) void {
std.sort.sort([]const u8, in, {}, cmpStringsDesc);
}
+pub fn isASCIIHexDigit(c: u8) bool {
+ return std.ascii.isDigit(c) or std.ascii.isXDigit(c);
+}
+
+pub fn toASCIIHexValue(character: u8) u8 {
+ std.debug.assert(isASCIIHexDigit(character));
+ return switch (character) {
+ 0...('A' - 1) => character - '0',
+ else => (character - 'A' + 10) & 0xF,
+ };
+}
+
pub fn utf8ByteSequenceLength(first_byte: u8) u3 {
// The switch is optimized much better than a "smart" approach using @clz
return switch (first_byte) {