ddapi_rs/util/encoding.rs
1use std::borrow::Cow;
2use std::fmt::Write;
3
4const NON_ASCII_CHARACTER_THRESHOLD: u32 = 128;
5
6#[inline]
7fn is_slugify2_symbol(c: char) -> bool {
8 matches!(
9 c,
10 '\t' | ' '
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/// Converts a nickname to a URL-safe slug format for API requests
44///
45/// This function handles special characters and non-ASCII characters in nicknames
46/// by encoding them into a format that can be safely used in URLs. Characters that
47/// are not ASCII or are one of the slugify2 separator symbols are converted to their Unicode code points
48/// surrounded by hyphens.
49///
50/// # Arguments
51///
52/// * `nickname` - The player nickname to slugify
53///
54/// # Returns
55///
56/// Returns `Cow<'_, str>` - Borrowed if no conversion needed, Owned if conversion occurred
57///
58/// # Examples
59///
60/// ```
61/// use ddapi_rs::prelude::slugify2;
62///
63/// // ASCII-only nicknames without special symbols are returned as-is
64/// assert_eq!(slugify2("Player1"), "Player1");
65///
66/// // Special symbols and non-ASCII characters are encoded
67/// assert_eq!(slugify2("Player@"), "Player-64-");
68/// assert_eq!(slugify2("玩家"), "-29609--23478-");
69///
70/// // Mixed characters
71/// assert_eq!(slugify2("Test_Player"), "Test-95-Player");
72/// ```
73pub fn slugify2(nickname: &str) -> Cow<'_, str> {
74 let needs_processing = nickname
75 .chars()
76 .any(|c| is_slugify2_symbol(c) || (c as u32) >= NON_ASCII_CHARACTER_THRESHOLD);
77
78 if !needs_processing {
79 return Cow::Borrowed(nickname);
80 }
81
82 let mut result = String::with_capacity(nickname.len() * 4);
83
84 for c in nickname.chars() {
85 if is_slugify2_symbol(c) || (c as u32) >= NON_ASCII_CHARACTER_THRESHOLD {
86 write!(&mut result, "-{}-", c as u32).unwrap();
87 } else {
88 result.push(c);
89 }
90 }
91
92 Cow::Owned(result)
93}
94
95/// Encodes a nickname for safe use in URLs
96///
97/// This function ensures that nicknames containing special characters, spaces,
98/// or non-ASCII characters are properly URL-encoded. ASCII nicknames without
99/// control characters are returned as-is for better performance.
100///
101/// # Arguments
102///
103/// * `nickname` - The player nickname to URL-encode
104///
105/// # Returns
106///
107/// Returns `Cow<'_, str>` -
108/// - `Cow::Borrowed` if the nickname is already URL-safe (ASCII without control characters)
109/// - `Cow::Owned` with URL-encoded string if encoding is required
110///
111/// # Examples
112///
113/// ```
114/// use ddapi_rs::prelude::encode;
115///
116/// // Safe ASCII nicknames are returned without changes
117/// assert_eq!(encode("Player1"), "Player1");
118/// assert_eq!(encode("abc_XYZ"), "abc_XYZ");
119///
120/// // Characters requiring encoding are properly handled
121/// assert_eq!(encode("Player Server"), "Player%20Server");
122/// assert_eq!(encode("Player@Server"), "Player%40Server");
123/// assert_eq!(encode("玩家"), "%E7%8E%A9%E5%AE%B6");
124/// assert_eq!(encode("emoji🎮"), "emoji%F0%9F%8E%AE");
125///
126/// // Special cases
127/// assert_eq!(encode(""), "");
128/// assert_eq!(encode("a b"), "a%20b");
129/// ```
130pub fn encode(nickname: &str) -> Cow<'_, str> {
131 // RFC 3986 unreserved characters: ALPHA / DIGIT / "-" / "." / "_" / "~"
132 // If already unreserved-only, return a borrow.
133 if nickname
134 .bytes()
135 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~'))
136 {
137 return Cow::Borrowed(nickname);
138 }
139
140 // Percent-encode UTF-8 bytes. This avoids pulling in a dependency for a tiny operation.
141 let mut out = String::with_capacity(nickname.len() * 3);
142 for &b in nickname.as_bytes() {
143 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
144 out.push(b as char);
145 } else {
146 out.push('%');
147 out.push(hex_upper(b >> 4));
148 out.push(hex_upper(b & 0x0f));
149 }
150 }
151 Cow::Owned(out)
152}
153
154#[inline]
155fn hex_upper(n: u8) -> char {
156 debug_assert!(n < 16);
157 match n {
158 0..=9 => (b'0' + n) as char,
159 10..=15 => (b'A' + (n - 10)) as char,
160 _ => unreachable!(),
161 }
162}