Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions std/collections.pith
Original file line number Diff line number Diff line change
Expand Up @@ -570,12 +570,15 @@ pub fn join_with(list: List[String], sep: String) -> String:
n := list.len()
if n == 0:
return ""
mut result := list[0]
mut i := 1
# copy into a local list and join once: appending each element to the
# result string recopied everything accumulated so far, which made the
# join quadratic in the total length.
mut parts: List[String] := []
mut i := 0
while i < n:
result = result + sep + list[i]
parts.push(list[i])
i = i + 1
return result
return parts.join(sep)

test "collection constructors and aggregations work":
nums := range(1, 5)
Expand Down
15 changes: 9 additions & 6 deletions std/config.pith
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,24 @@ fn config_json_skip_ws(input: String, position: Int) -> Int:

fn config_json_read_string(input: String, position: Int) -> ConfigStringScan:
mut pos := position
mut out := ""
if pos >= input.len() or input[pos] != chr(34):
return ConfigStringScan(out, pos)
return ConfigStringScan("", pos)
# collect the pieces and join once at the end: appending each character
# to the result string recopied everything accumulated so far, which
# made reading a long string value quadratic in its length.
mut parts: List[String] := []
pos = pos + 1
while pos < input.len():
if input[pos] == chr(92):
if pos + 1 < input.len():
out = out + input[pos + 1]
parts.push(input[pos + 1])
pos = pos + 2
continue
if input[pos] == chr(34):
return ConfigStringScan(out, pos + 1)
out = out + input[pos]
return ConfigStringScan(parts.join(""), pos + 1)
parts.push(input[pos])
pos = pos + 1
return ConfigStringScan(out, pos)
return ConfigStringScan(parts.join(""), pos)

fn config_json_starts_at(input: String, position: Int, text: String) -> Bool:
if position + text.len() > input.len():
Expand Down
50 changes: 37 additions & 13 deletions std/encoding.pith
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,12 @@ fn encoding_b64_decode_utf8(input: String) -> String!:
return b64_decode_utf8(input)

fn b32_encode_with(input: Bytes, table: String, padded: Bool) -> String:
mut result := ""
# appending one alphabet character at a time to the result string
# recopied the whole accumulated string, which made base32 encoding
# quadratic in the input; a byte buffer keeps each append constant.
alphabet := bytes.from_string_utf8(table)
out := bytes.buffer_with_capacity((input.len() + 4) / 5 * 8)
defer out.free()
mut buffer := 0
mut bits_left := 0
mut i := 0
Expand All @@ -244,17 +249,20 @@ fn b32_encode_with(input: Bytes, table: String, padded: Bool) -> String:
bits_left = bits_left + 8
while bits_left >= 5:
index := buffer / pow2(bits_left - 5)
result = result + table[index % 32]
out.write_byte(alphabet[index % 32])
bits_left = bits_left - 5
buffer = buffer % pow2(bits_left)
i = i + 1
if bits_left > 0:
index := buffer * pow2(5 - bits_left)
result = result + table[index % 32]
out.write_byte(alphabet[index % 32])
if padded:
while result.len() % 8 != 0:
result = result + "="
return result
while out.len() % 8 != 0:
out.write_word(PAD_BYTE, 1)
# every byte written above came from an ascii alphabet or is "=", so the
# decode cannot fail; an empty result would be a visible total failure
# rather than a silently wrong encoding.
return out.bytes().to_string_utf8() catch ""

fn b32_decode_with(input: String, table: String, padded: Bool, ignore_ws: Bool) -> Bytes!:
cleaned := clean_encoded(input, ignore_ws)!
Expand Down Expand Up @@ -345,19 +353,27 @@ pub fn base58_encode(input: Bytes) -> String:
carry = carry / 58
i = i + 1

mut result := ""
# appending one alphabet character at a time to the result string
# recopied the whole accumulated string, which made base58 encoding
# quadratic in the output; a byte buffer keeps each append constant.
table := bytes.from_string_utf8(alphabet)
out := bytes.buffer_with_capacity(zeroes + digits.len())
defer out.free()
i = 0
while i < zeroes:
result = result + alphabet[0]
out.write_byte(table[0])
i = i + 1

mut j := digits.len() - 1
while j >= 0 and digits.get(j) == 0:
j = j - 1
while j >= 0:
result = result + alphabet[digits.get(j)]
out.write_byte(table[digits.get(j)])
j = j - 1
return result
# every byte written above came from an ascii alphabet, so the decode
# cannot fail; an empty result would be a visible total failure rather
# than a silently wrong encoding.
return out.bytes().to_string_utf8() catch ""

# decode bitcoin base58 into bytes.
pub fn base58_decode(input: String) -> Bytes!:
Expand Down Expand Up @@ -413,13 +429,21 @@ fn hex_nibble(c: String) -> Int:
return - 1

fn hex_with(input: Bytes, digits: String) -> String:
mut result := ""
# appending each digit pair to the result string recopied the whole
# accumulated string, which made hex encoding quadratic in the input.
# both digits of a byte pack into one two-byte buffer append.
table := bytes.from_string_utf8(digits)
out := bytes.buffer_with_capacity(input.len() * 2)
defer out.free()
mut position := 0
while position < input.len():
byte_value := input[position]
result = result + digits[byte_value / 16] + digits[byte_value % 16]
out.write_word(table[byte_value / 16] + table[byte_value % 16] * 256, 2)
position = position + 1
return result
# every byte written above came from an ascii digit table, so the decode
# cannot fail; an empty result would be a visible total failure rather
# than a silently wrong encoding.
return out.bytes().to_string_utf8() catch ""

# encode bytes as lowercase hexadecimal.
pub fn to_hex(input: Bytes) -> String:
Expand Down
24 changes: 16 additions & 8 deletions std/html.pith
Original file line number Diff line number Diff line change
Expand Up @@ -107,23 +107,28 @@ fn entity_for(byte: Int) -> String:
# >= 0x80, so no character is ever split by an entity and non-ascii text is
# copied through whole.
pub fn escape(input: String) -> String:
# collect the runs and entities in a list and join once at the end:
# appending each piece to the result string recopied everything
# accumulated so far, which made escaping quadratic in the number of
# entities — input that alternates a plain character with an escaped one
# hit the worst case.
length := input.len()
mut out := ""
mut parts: List[String] := []
mut copied := 0
mut position := 0
while position < length:
entity := entity_for(ord(input[position]))
if entity != "":
if position > copied:
out = out + input.substring(copied, position)
out = out + entity
parts.push(input.substring(copied, position))
parts.push(entity)
copied = position + 1
position = position + 1
if copied == 0:
return input
if copied < length:
out = out + input.substring(copied, length)
return out
parts.push(input.substring(copied, length))
return parts.join("")

# Whether `input` contains a character `escape` would rewrite.
#
Expand Down Expand Up @@ -161,14 +166,17 @@ fn scheme_allowed(scheme: String) -> Bool:
# happily run. stripping first means the allowlist sees the same scheme the
# browser will.
fn strip_url_noise(input: String) -> String:
mut out := ""
# collect the kept characters and join once at the end: appending each
# one to the result string recopied everything accumulated so far, which
# made stripping quadratic in the url.
mut parts: List[String] := []
mut position := 0
while position < input.len():
byte := ord(input[position])
if byte > 32 and byte != 127:
out = out + input[position]
parts.push(input[position])
position = position + 1
return out
return parts.join("")

# true when `input` begins with a sequence a browser may read as the start of
# a protocol-relative url, which navigates off-site under the current scheme.
Expand Down
39 changes: 21 additions & 18 deletions std/json.pith
Original file line number Diff line number Diff line change
Expand Up @@ -412,66 +412,69 @@ fn json_utf8_encode(code: Int) -> String:
# quotes). returns the decoded text and whether every escape was valid.
# rfc 8259 escapes only; a \u surrogate pair combines into one code point.
fn json_unescape_text(raw: String) -> (String, Bool):
mut out := ""
# collect the pieces and join once at the end: appending to the result
# string one character at a time recopied everything accumulated so far
# on every append, so a string full of escapes decoded in quadratic time.
mut parts: List[String] := []
mut i := 0
while i < raw.len():
c := raw[i]
if c != chr(92):
out = out + c
parts.push(c)
i = i + 1
continue
if i + 1 >= raw.len():
return (out, false)
return (parts.join(""), false)
e := raw[i + 1]
if e == chr(34) or e == chr(92) or e == "/":
out = out + e
parts.push(e)
elif e == "b":
out = out + chr(8)
parts.push(chr(8))
elif e == "f":
out = out + chr(12)
parts.push(chr(12))
elif e == "n":
out = out + chr(10)
parts.push(chr(10))
elif e == "r":
out = out + chr(13)
parts.push(chr(13))
elif e == "t":
out = out + chr(9)
parts.push(chr(9))
elif e == "u":
if i + 5 >= raw.len():
return (out, false)
return (parts.join(""), false)
mut code := 0
mut d := 0
while d < 4:
digit := json_hex_digit_value(raw[i + 2 + d])
if digit < 0:
return (out, false)
return (parts.join(""), false)
code = code * 16 + digit
d = d + 1
i = i + 6
# a high surrogate must be followed by \u and a low surrogate;
# the pair combines into one supplementary code point.
if code >= 0xD800 and code <= 0xDBFF:
if i + 5 >= raw.len() or raw[i] != chr(92) or raw[i + 1] != "u":
return (out, false)
return (parts.join(""), false)
mut low := 0
d = 0
while d < 4:
digit := json_hex_digit_value(raw[i + 2 + d])
if digit < 0:
return (out, false)
return (parts.join(""), false)
low = low * 16 + digit
d = d + 1
if low < 0xDC00 or low > 0xDFFF:
return (out, false)
return (parts.join(""), false)
code = 0x10000 + (code - 0xD800) * 1024 + (low - 0xDC00)
i = i + 6
elif code >= 0xDC00 and code <= 0xDFFF:
return (out, false)
out = out + json_utf8_encode(code)
return (parts.join(""), false)
parts.push(json_utf8_encode(code))
continue
else:
return (out, false)
return (parts.join(""), false)
i = i + 2
return (out, true)
return (parts.join(""), true)

fn json_byte_read_string(input: Bytes, pos: Int) -> JsonByteString:
if pos < 0 or pos >= input.len() or input[pos] != 34:
Expand Down
30 changes: 18 additions & 12 deletions std/log.pith
Original file line number Diff line number Diff line change
Expand Up @@ -354,20 +354,23 @@ fn needs_console_quotes(value: String) -> Bool:
return value.contains(" ") or value.contains(chr(9)) or value.contains(chr(10)) or value.contains(chr(34))

fn console_escape(value: String) -> String:
mut out := ""
# collect the pieces and join once at the end: appending each character
# to the result string recopied everything accumulated so far, which
# made escaping quadratic in the value.
mut parts: List[String] := []
mut i := 0
while i < value.len():
c := value[i]
if c == chr(34):
out = out + chr(92) + chr(34)
parts.push(chr(92) + chr(34))
elif c == chr(92):
out = out + chr(92) + chr(92)
parts.push(chr(92) + chr(92))
elif c == chr(10):
out = out + chr(92) + "n"
parts.push(chr(92) + "n")
else:
out = out + c
parts.push(c)
i = i + 1
return out
return parts.join("")

fn render_console_field(field: Field) -> String:
if field.kind == "raw":
Expand All @@ -390,20 +393,23 @@ fn render_console_fields(fields: List[Field]) -> String:
return " " + parts.join(" ")

fn json_escape(text: String) -> String:
mut out := ""
# collect the pieces and join once at the end: appending each character
# to the result string recopied everything accumulated so far, which
# made escaping quadratic in the text.
mut parts: List[String] := []
mut i := 0
while i < text.len():
c := text[i]
if c == chr(34):
out = out + chr(92) + chr(34)
parts.push(chr(92) + chr(34))
elif c == chr(92):
out = out + chr(92) + chr(92)
parts.push(chr(92) + chr(92))
elif c == chr(10):
out = out + chr(92) + "n"
parts.push(chr(92) + "n")
else:
out = out + c
parts.push(c)
i = i + 1
return out
return parts.join("")

fn json_string_field(name: String, value: String) -> String:
return chr(34) + name + chr(34) + ":" + chr(34) + json_escape(value) + chr(34)
Expand Down
Loading
Loading