Skip to main content

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}