From 512fb28512428b3458bf6256bbcd698fbdb16dc2 Mon Sep 17 00:00:00 2001 From: Divan Burger Date: Sun, 5 Nov 2023 21:06:18 +0200 Subject: [PATCH] Add the form data variant of percent_encode/decode --- core/net/url.odin | 72 +++++++++++++++++++++++++++++++++++++++ core/strings/strings.odin | 47 +++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/core/net/url.odin b/core/net/url.odin index ef43d6c9fda..99a72b98bbd 100644 --- a/core/net/url.odin +++ b/core/net/url.odin @@ -160,6 +160,78 @@ percent_decode :: proc(encoded_string: string, allocator := context.allocator) - return } +// For data with content type application/x-www-form-urlencoded +form_data_encode :: proc(s: string, allocator := context.allocator) -> string { + b := strings.builder_make(allocator) + strings.builder_grow(&b, len(s) + 16) // NOTE(tetra): A reasonable number to allow for the number of things we need to escape. + + for ch in s { + switch ch { + case 'A'..='Z', 'a'..='z', '0'..='9', '-', '_', '.', '~': + strings.write_rune(&b, ch) + case ' ': + strings.write_rune(&b, '+') + case: + bytes, n := utf8.encode_rune(ch) + for byte in bytes[:n] { + buf: [2]u8 = --- + t := strconv.append_int(buf[:], i64(byte), 16) + strings.write_rune(&b, '%') + strings.write_string(&b, t) + } + } + } + + return strings.to_string(b) +} + +form_data_decode :: proc(encoded_string: string, allocator := context.allocator) -> (decoded_string: string, ok: bool) { + b := strings.builder_make(allocator) + strings.builder_grow(&b, len(encoded_string)) + defer if !ok do strings.builder_destroy(&b) + + s := encoded_string + + for len(s) > 0 { + i := strings.index_byte_any(s, {'%', '+'}) + if i == -1 { + strings.write_string(&b, s) // no '%'s; the string is already decoded + break + } + + strings.write_string(&b, s[:i]) + + if s[i] == '+' { + strings.write_byte(&b, ' ') + s = s[i+1:] + continue + } + + s = s[i:] + + if len(s) == 0 do return // percent without anything after it + s = s[1:] + + if s[0] == '%' { + strings.write_byte(&b, '%') + s = s[1:] + continue + } + + if len(s) < 2 { + return // percent without encoded value + } + + val := hex.decode_sequence(s[:2]) or_return + strings.write_byte(&b, val) + s = s[2:] + } + + ok = true + decoded_string = strings.to_string(b) + return +} + // // TODO: encoding/base64 is broken... // diff --git a/core/strings/strings.odin b/core/strings/strings.odin index 2f36eddbebb..b3de445ead6 100644 --- a/core/strings/strings.odin +++ b/core/strings/strings.odin @@ -1430,6 +1430,53 @@ index_byte :: proc(s: string, c: byte) -> (res: int) { return -1 } /* +Returns the byte offset of the first byte in the string s which is in `cs` that it finds, -1 when not found. +NOTE: Can't find UTF-8 based runes. + +Inputs: +- s: The input string to search in. +- cs: The bytes to search for. + +Returns: +- res: The byte offset of the first occurrence of any of `cs` in `s`, or -1 if not found. + +Example: + + import "core:fmt" + import "core:strings" + + index_byte_any_example :: proc() { + fmt.println(strings.index_byte_any("test", {'t'})) + fmt.println(strings.index_byte_any("test", {'e'})) + fmt.println(strings.index_byte_any("test", {'e', 't'})) + fmt.println(strings.index_byte_any("test", {'e', 's'})) + fmt.println(strings.index_byte_any("test", {'x'})) + fmt.println(strings.index_byte_any("test", {'s', 'x'})) + fmt.println(strings.index_byte_any("teäst", {'ä'})) + } + +Output: + + 0 + 1 + 0 + 1 + -1 + 2 + -1 + +*/ +index_byte_any :: proc(s: string, cs: []byte) -> (res: int) { + for i := 0; i < len(s); i += 1 { + for c in cs { + if s[i] == c { + return i + } + } + } + return -1 +} +/* Returns the byte offset of the last byte `c` in the string `s`, -1 when not found. Inputs: