104 lines
2.8 KiB
V
104 lines
2.8 KiB
V
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
|
||
// Use of this source code is governed by an MIT license
|
||
// that can be found in the LICENSE file.
|
||
module s3
|
||
|
||
// uri_encode performs RFC 3986 percent-encoding as required by Signature V4.
|
||
// Only the unreserved set A–Z / a–z / 0–9 / '-' / '_' / '.' / '~' is preserved.
|
||
// When `encode_slash` is false (used for object keys), '/' is left intact and
|
||
// backslashes are normalized to '/' so Windows-style paths produce the same
|
||
// canonical key. All other bytes are emitted as %XX with uppercase hex digits.
|
||
pub fn uri_encode(input string, encode_slash bool) string {
|
||
mut out := []u8{cap: input.len + (input.len >> 2)} // 25% headroom
|
||
for b in input.bytes() {
|
||
match b {
|
||
`A`...`Z`, `a`...`z`, `0`...`9`, `-`, `_`, `.`, `~` {
|
||
out << b
|
||
}
|
||
`/`, `\\` {
|
||
if encode_slash {
|
||
append_percent(mut out, b)
|
||
} else {
|
||
out << if b == `\\` { u8(`/`) } else { b }
|
||
}
|
||
}
|
||
else {
|
||
append_percent(mut out, b)
|
||
}
|
||
}
|
||
}
|
||
return out.bytestr()
|
||
}
|
||
|
||
// uri_encode_path encodes an S3 object key segment-aware: '/' is preserved
|
||
// because S3 paths use it as the segment separator.
|
||
@[inline]
|
||
pub fn uri_encode_path(path string) string {
|
||
return uri_encode(path, false)
|
||
}
|
||
|
||
// uri_encode_query encodes a value that will appear inside a query string.
|
||
// Slashes must be percent-encoded.
|
||
@[inline]
|
||
pub fn uri_encode_query(value string) string {
|
||
return uri_encode(value, true)
|
||
}
|
||
|
||
// strip_slashes removes leading and trailing '/' or '\\' separators. S3
|
||
// canonical paths must contain a single leading slash, no trailing one.
|
||
pub fn strip_slashes(s string) string {
|
||
if s == '' {
|
||
return s
|
||
}
|
||
mut start := 0
|
||
mut end := s.len
|
||
for start < end && (s[start] == `/` || s[start] == `\\`) {
|
||
start++
|
||
}
|
||
for end > start && (s[end - 1] == `/` || s[end - 1] == `\\`) {
|
||
end--
|
||
}
|
||
return s[start..end]
|
||
}
|
||
|
||
// contains_crlf returns true if `value` contains a CR or LF byte. Header
|
||
// values that pass user-provided strings (ACL, content-type, …) MUST be
|
||
// checked to prevent HTTP header injection (CRLF smuggling).
|
||
@[inline]
|
||
pub fn contains_crlf(value string) bool {
|
||
for b in value.bytes() {
|
||
if b == `\r` || b == `\n` {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// to_hex_lower formats raw bytes as their lowercase hex string. Used for
|
||
// SHA-256 digests inside SigV4 (the spec requires lowercase).
|
||
pub fn to_hex_lower(data []u8) string {
|
||
mut out := []u8{len: data.len * 2}
|
||
for i, b in data {
|
||
out[i * 2] = hex_lower_nibble(b >> 4)
|
||
out[i * 2 + 1] = hex_lower_nibble(b & 0x0F)
|
||
}
|
||
return out.bytestr()
|
||
}
|
||
|
||
@[inline]
|
||
fn append_percent(mut out []u8, b u8) {
|
||
out << `%`
|
||
out << hex_upper_nibble(b >> 4)
|
||
out << hex_upper_nibble(b & 0x0F)
|
||
}
|
||
|
||
@[inline]
|
||
fn hex_upper_nibble(n u8) u8 {
|
||
return if n < 10 { n + `0` } else { n - 10 + `A` }
|
||
}
|
||
|
||
@[inline]
|
||
fn hex_lower_nibble(n u8) u8 {
|
||
return if n < 10 { n + `0` } else { n - 10 + `a` }
|
||
}
|