diff options
| author | 2021-08-06 23:28:13 -0700 | |
|---|---|---|
| committer | 2021-08-06 23:28:13 -0700 | |
| commit | 4b1f89114e02f8472b63794534698ec13929080e (patch) | |
| tree | 8e038d24c11a8d4b471cb72fc86de135ec3042e5 | |
| parent | 6e4da63abeb76540bfefa9efcb3d823f8b50eb61 (diff) | |
| download | bun-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.tsx | 1 | ||||
| -rw-r--r-- | src/javascript/jsc/api/router.zig | 55 | ||||
| -rw-r--r-- | src/javascript/jsc/bindings/bindings.cpp | 103 | ||||
| -rw-r--r-- | src/javascript/jsc/bindings/bindings.zig | 39 | ||||
| -rw-r--r-- | src/javascript/jsc/bindings/headers-cpp.h | 2 | ||||
| -rw-r--r-- | src/javascript/jsc/bindings/headers.h | 6 | ||||
| -rw-r--r-- | src/javascript/jsc/bindings/headers.zig | 4 | ||||
| -rw-r--r-- | src/query_string_map.zig | 652 | ||||
| -rw-r--r-- | src/string_immutable.zig | 12 |
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) { |
