steam_friend_code/
csgo_friend_code.rs1use std::{fmt, ops::Deref, str::FromStr};
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct CsgoFriendCode(String);
13
14impl CsgoFriendCode {
15 pub fn new(code: String) -> Self {
16 Self(code)
17 }
18
19 pub fn from_steam_id(steam_id: u64) -> Self {
20 let mut steam_id = steam_id;
21 let h = hash_steam_id(steam_id);
22
23 let mut r = 0u64;
24 for i in 0..8 {
25 let id_nibble = steam_id & 0xF;
26 steam_id >>= 4;
27
28 let hash_nibble = (h >> i) & 1;
29
30 let a = (r << 4) | id_nibble;
31
32 r = make_u64(r >> 28, a);
33 r = make_u64(r >> 31, (a << 1) | hash_nibble);
34 }
35
36 let mut res = b32(r);
37
38 if res.starts_with("AAAA-") {
39 res = res[5..].to_string();
40 }
41
42 Self(res)
43 }
44
45 pub fn to_account_id(&self) -> Option<u32> {
46 let code_clean = self.0.replace("-", "");
47
48 let full_code = match code_clean.len() {
51 13 => code_clean,
52 9 => format!("AAAA{}", code_clean),
53 _ => return None,
54 };
55
56 let mut val = b32_decode(&full_code)?;
57
58 val = val.swap_bytes();
60
61 let mut id: u32 = 0;
67 let mut hash_bits: u8 = 0;
68
69 for i in 0..8 {
70 let i_rev = 7 - i;
73
74 let block = val & 0x1F;
75 val >>= 5;
76
77 let id_nibble = (block >> 1) as u32;
78 let hash_bit = (block & 1) as u8;
79
80 id |= id_nibble << (i_rev * 4);
81 hash_bits |= hash_bit << i_rev;
82 }
83
84 let expected_hash = hash_steam_id(id as u64);
86 if (expected_hash & 0xFF) as u8 == hash_bits {
87 Some(id)
88 } else {
89 None
90 }
91 }
92
93 pub fn is_valid(&self) -> bool {
94 self.to_account_id().is_some()
95 }
96}
97
98impl Deref for CsgoFriendCode {
99 type Target = str;
100
101 fn deref(&self) -> &Self::Target {
102 &self.0
103 }
104}
105
106impl FromStr for CsgoFriendCode {
107 type Err = ();
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 let s = s.trim().to_uppercase();
111 let code = CsgoFriendCode(s);
112 if code.is_valid() {
113 Ok(code)
114 } else {
115 Err(())
116 }
117 }
118}
119
120impl From<u64> for CsgoFriendCode {
121 fn from(steam_id: u64) -> Self {
122 Self::from_steam_id(steam_id)
123 }
124}
125
126impl From<String> for CsgoFriendCode {
127 fn from(s: String) -> Self {
128 Self(s)
129 }
130}
131
132impl fmt::Display for CsgoFriendCode {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(f, "{}", self.0)
135 }
136}
137
138fn make_u64(hi: u64, lo: u64) -> u64 {
139 (hi << 32) | lo
140}
141
142fn hash_steam_id(id: u64) -> u64 {
143 let account_id = id & 0xFFFFFFFF;
144 let strange_steam_id = account_id | 0x4353474F00000000; let bytes = strange_steam_id.to_le_bytes();
147 let digest = md5::compute(bytes);
148
149 let slice: [u8; 4] = digest[0..4].try_into().expect("slice with incorrect length");
151 u32::from_le_bytes(slice) as u64
152}
153
154fn b32(input: u64) -> String {
155 let mut input = input;
156 let alnum = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
157 let mut res = String::new();
158
159 input = input.swap_bytes();
163
164 for i in 0..13 {
165 if i == 4 || i == 9 {
166 res.push('-');
167 }
168 let index = (input & 0x1F) as usize;
169 res.push(alnum[index] as char);
170 input >>= 5;
171 }
172
173 res
174}
175
176fn b32_decode(input: &str) -> Option<u64> {
177 let alnum = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
178 let mut res = 0u64;
179 let mut shift = 0;
180
181 for c in input.bytes() {
182 let pos = alnum.iter().position(|&x| x == c)?;
183 res |= (pos as u64) << shift;
184 shift += 5;
185 }
186
187 Some(res)
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_steam_friend_code_serialization() {
196 let code = CsgoFriendCode::new("ABCDE-12345".to_string());
197 let json = serde_json::to_string(&code).unwrap();
198 assert_eq!(json, "\"ABCDE-12345\"");
199
200 let decoded: CsgoFriendCode = serde_json::from_str(&json).unwrap();
201 assert_eq!(decoded, code);
202 }
203
204 #[test]
205 fn test_friend_code_conversion() {
206 let steam_id = 76561197960287930u64;
207 let code = CsgoFriendCode::from_steam_id(steam_id);
208 println!("Code: {}", code);
209 let decoded = code.to_account_id();
210 assert_eq!(decoded, Some(22202));
211 }
212
213 #[test]
214 fn test_roundtrip_random() {
215 let ids = vec![12345, 999999, 1, 0, 2147483647];
216 for id in ids {
217 let full_id = id as u64 | 0x0110000100000000;
218 let code = CsgoFriendCode::from_steam_id(full_id);
219 assert_eq!(code.to_account_id(), Some(id), "Failed for id {} code {}", id, code);
220 }
221 }
222
223 #[test]
224 fn test_invalid_code() {
225 let code = CsgoFriendCode("INVALID-CODE".to_string());
226 assert_eq!(code.to_account_id(), None);
227 }
228
229 #[test]
230 fn test_from_str() {
231 let id = 12345;
232 let full_id = id as u64 | 0x0110000100000000;
233 let code = CsgoFriendCode::from_steam_id(full_id);
234 let s = code.to_string();
235
236 let parsed: Result<CsgoFriendCode, _> = s.parse();
237 assert!(parsed.is_ok());
238 assert_eq!(parsed.unwrap().to_account_id(), Some(id));
239
240 assert!(CsgoFriendCode::from_str("INVALID").is_err());
241 }
242}