1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//! URL encoding helpers.
//!
//! `percent_encode_unreserved` implements the RFC 3986 unreserved-character
//! set used by OAuth 1.0 signatures and generic URL path segments. Prior
//! duplicates lived in `stage-announce/src/twitter.rs` and
//! `cli/src/commands/release/milestones.rs` with byte-equivalent but
//! independently-defined character sets.
use percent_encoding::{AsciiSet, CONTROLS, NON_ALPHANUMERIC, utf8_percent_encode};
/// RFC 3986 unreserved set: `A-Z a-z 0-9 - _ . ~`. Every other byte is
/// encoded as `%XX`.
const UNRESERVED: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'!')
.add(b'"')
.add(b'#')
.add(b'$')
.add(b'%')
.add(b'&')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b'+')
.add(b',')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'=')
.add(b'>')
.add(b'?')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
/// Percent-encode every byte that isn't in the RFC 3986 unreserved set
/// (`A-Z a-z 0-9 - _ . ~`). Used for OAuth 1.0 signature base strings and
/// generic URL path/query segments where only unreserved chars pass through.
pub fn percent_encode_unreserved(s: &str) -> String {
utf8_percent_encode(s, UNRESERVED).to_string()
}
/// Encode set for a single URL path segment: everything that isn't alphanumeric
/// or one of `- _ .` is percent-encoded. Notably `+`, `#`, `?`, `/`, space, and
/// all other reserved characters are encoded — safe for tag names, owner/repo
/// names, file names, and GitLab project-id path segments (where `/` must
/// become `%2F`).
const PATH_SEGMENT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'-').remove(b'_').remove(b'.');
/// Percent-encode a single URL path segment.
///
/// Keeps only `A-Z a-z 0-9 - _ .`. Used for tags, owner/repo names, package
/// names, versions, and file names in release backend URLs so that identifiers
/// like `v1.0.0+build.1` or `group/project` are safely encoded (`+` → `%2B`,
/// `/` → `%2F`). Unifies previously-duplicated sets in the GitHub/GitLab/Gitea
/// release backends that produced diverging URLs for the same tag.
pub fn percent_encode_path_segment(s: &str) -> String {
utf8_percent_encode(s, PATH_SEGMENT).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unreserved_passes_through() {
assert_eq!(percent_encode_unreserved("hello"), "hello");
assert_eq!(percent_encode_unreserved("A-Za-z0-9-_.~"), "A-Za-z0-9-_.~");
}
#[test]
fn space_and_specials_encoded() {
assert_eq!(percent_encode_unreserved("hello world"), "hello%20world");
assert_eq!(percent_encode_unreserved("a=b&c=d"), "a%3Db%26c%3Dd");
}
#[test]
fn slashes_encoded() {
assert_eq!(percent_encode_unreserved("a/b/c"), "a%2Fb%2Fc");
}
#[test]
fn utf8_encoded_per_byte() {
// é = 0xC3 0xA9 in UTF-8
assert_eq!(percent_encode_unreserved("café"), "caf%C3%A9");
}
}