v2 / vlib / net / s3 / encoding.v
103 lines · 94 sloc · 2.85 KB · 4142432483c4e8de44ab7b0d6ac944f3251e03c8
Raw
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.
4module 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.
11pub 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]
36pub 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]
43pub 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.
49pub 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]
68pub 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).
79pub 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]
89fn 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]
96fn hex_upper_nibble(n u8) u8 {
97 return if n < 10 { n + `0` } else { n - 10 + `A` }
98}
99
100@[inline]
101fn hex_lower_nibble(n u8) u8 {
102 return if n < 10 { n + `0` } else { n - 10 + `a` }
103}
104