| 1 | // Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | module s3 |
| 5 | |
| 6 | // uri_encode performs RFC 3986 percent-encoding as required by Signature V4. |
| 7 | // Only the unreserved set A–Z / a–z / 0–9 / '-' / '_' / '.' / '~' is preserved. |
| 8 | // When `encode_slash` is false (used for object keys), '/' is left intact and |
| 9 | // backslashes are normalized to '/' so Windows-style paths produce the same |
| 10 | // canonical key. All other bytes are emitted as %XX with uppercase hex digits. |
| 11 | pub fn uri_encode(input string, encode_slash bool) string { |
| 12 | mut out := []u8{cap: input.len + (input.len >> 2)} // 25% headroom |
| 13 | for b in input.bytes() { |
| 14 | match b { |
| 15 | `A`...`Z`, `a`...`z`, `0`...`9`, `-`, `_`, `.`, `~` { |
| 16 | out << b |
| 17 | } |
| 18 | `/`, `\\` { |
| 19 | if encode_slash { |
| 20 | append_percent(mut out, b) |
| 21 | } else { |
| 22 | out << if b == `\\` { u8(`/`) } else { b } |
| 23 | } |
| 24 | } |
| 25 | else { |
| 26 | append_percent(mut out, b) |
| 27 | } |
| 28 | } |
| 29 | } |
| 30 | return out.bytestr() |
| 31 | } |
| 32 | |
| 33 | // uri_encode_path encodes an S3 object key segment-aware: '/' is preserved |
| 34 | // because S3 paths use it as the segment separator. |
| 35 | @[inline] |
| 36 | pub fn uri_encode_path(path string) string { |
| 37 | return uri_encode(path, false) |
| 38 | } |
| 39 | |
| 40 | // uri_encode_query encodes a value that will appear inside a query string. |
| 41 | // Slashes must be percent-encoded. |
| 42 | @[inline] |
| 43 | pub fn uri_encode_query(value string) string { |
| 44 | return uri_encode(value, true) |
| 45 | } |
| 46 | |
| 47 | // strip_slashes removes leading and trailing '/' or '\\' separators. S3 |
| 48 | // canonical paths must contain a single leading slash, no trailing one. |
| 49 | pub fn strip_slashes(s string) string { |
| 50 | if s == '' { |
| 51 | return s |
| 52 | } |
| 53 | mut start := 0 |
| 54 | mut end := s.len |
| 55 | for start < end && (s[start] == `/` || s[start] == `\\`) { |
| 56 | start++ |
| 57 | } |
| 58 | for end > start && (s[end - 1] == `/` || s[end - 1] == `\\`) { |
| 59 | end-- |
| 60 | } |
| 61 | return s[start..end] |
| 62 | } |
| 63 | |
| 64 | // contains_crlf returns true if `value` contains a CR or LF byte. Header |
| 65 | // values that pass user-provided strings (ACL, content-type, …) MUST be |
| 66 | // checked to prevent HTTP header injection (CRLF smuggling). |
| 67 | @[inline] |
| 68 | pub fn contains_crlf(value string) bool { |
| 69 | for b in value.bytes() { |
| 70 | if b == `\r` || b == `\n` { |
| 71 | return true |
| 72 | } |
| 73 | } |
| 74 | return false |
| 75 | } |
| 76 | |
| 77 | // to_hex_lower formats raw bytes as their lowercase hex string. Used for |
| 78 | // SHA-256 digests inside SigV4 (the spec requires lowercase). |
| 79 | pub fn to_hex_lower(data []u8) string { |
| 80 | mut out := []u8{len: data.len * 2} |
| 81 | for i, b in data { |
| 82 | out[i * 2] = hex_lower_nibble(b >> 4) |
| 83 | out[i * 2 + 1] = hex_lower_nibble(b & 0x0F) |
| 84 | } |
| 85 | return out.bytestr() |
| 86 | } |
| 87 | |
| 88 | @[inline] |
| 89 | fn append_percent(mut out []u8, b u8) { |
| 90 | out << `%` |
| 91 | out << hex_upper_nibble(b >> 4) |
| 92 | out << hex_upper_nibble(b & 0x0F) |
| 93 | } |
| 94 | |
| 95 | @[inline] |
| 96 | fn hex_upper_nibble(n u8) u8 { |
| 97 | return if n < 10 { n + `0` } else { n - 10 + `A` } |
| 98 | } |
| 99 | |
| 100 | @[inline] |
| 101 | fn hex_lower_nibble(n u8) u8 { |
| 102 | return if n < 10 { n + `0` } else { n - 10 + `a` } |
| 103 | } |
| 104 | |