1use std::num::ParseIntError;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4pub enum SteamIdKind {
5 Steam64,
6 Steam32,
7 Steam3,
8}
9
10pub fn parse_incoming_format(steam_id: &str) -> Result<SteamIdKind, SteamIdError> {
11 if steam_id.len() == 17 && steam_id.starts_with("7656119") {
12 return Ok(SteamIdKind::Steam64);
13 }
14
15 if steam_id.to_ascii_uppercase().starts_with("STEAM_") {
16 return Ok(SteamIdKind::Steam32);
17 }
18
19 if steam_id.starts_with("[U:") && steam_id.ends_with(']') {
20 return Ok(SteamIdKind::Steam3);
21 }
22
23 return Err(SteamIdError::InvalidFormat);
24}
25
26pub fn to_steam32(steam_id: &str) -> Result<String, SteamIdError> {
27 let incoming_format = parse_incoming_format(steam_id)?; match incoming_format {
30 SteamIdKind::Steam64 => {
31 let steamid64: u64 = steam_id.parse()?;
32
33 const UNIVERSE_BASE: u64 = 76561197960265728;
34 if steamid64 < UNIVERSE_BASE {
35 return Err(SteamIdError::InvalidFormat);
36 }
37
38 let account_id = steamid64 - UNIVERSE_BASE;
39 let middle_digit = account_id % 2;
40 let auth_server = account_id / 2;
41
42 Ok(format!("STEAM_0:{}:{}", middle_digit, auth_server))
43 }
44
45 SteamIdKind::Steam32 => Ok(steam_id.to_string()),
46
47 SteamIdKind::Steam3 => {
48 let input = steam_id.trim();
49
50 let cleaned = if input.starts_with('[') && input.ends_with(']') {
51 &input[1..input.len() - 1]
52 } else {
53 input
54 };
55
56 let parts: Vec<&str> = cleaned.split(':').collect();
57 if parts.len() != 3 {
58 return Err(SteamIdError::InvalidFormat);
59 }
60
61 if parts[0] != "U" {
62 return Err(SteamIdError::InvalidPrefix);
63 }
64
65 if parts[1] != "1" {
66 return Err(SteamIdError::InvalidFormat);
67 }
68
69 let account_id_str = parts[2];
70 let account_id: u32 = account_id_str.parse().map_err(SteamIdError::ParseError)?;
71 let y = account_id % 2;
72 let z = account_id / 2;
73
74 Ok(format!("STEAM_0:{}:{}", y, z))
75 }
76 }
77}
78
79pub fn to_steam64(steam_id: &str) -> Result<String, SteamIdError> {
80 let incoming_format = parse_incoming_format(steam_id)?;
81 match incoming_format {
82 SteamIdKind::Steam64 => Ok(steam_id.to_string()),
83 SteamIdKind::Steam32 => {
84 const STEAMID64_BASE: u64 = 76561197960265728;
85 let parts: Vec<&str> = steam_id.split(':').collect();
86 let y: u32 = parts[1].parse().map_err(|_| SteamIdError::InvalidY)?;
87
88 let z: u64 = parts[2].parse().map_err(|_| SteamIdError::InvalidZ)?;
89
90 if y > 1 {
91 return Err(SteamIdError::InvalidY);
92 }
93 let account_id = z * 2 + u64::from(y);
94
95 let steam64 = STEAMID64_BASE + account_id;
96
97 Ok(steam64.to_string())
98 }
99 SteamIdKind::Steam3 => {
100 let input = steam_id.trim();
101 let cleaned = if input.starts_with('[') && input.ends_with(']') {
102 &input[1..input.len() - 1]
103 } else {
104 input
105 };
106
107 let parts: Vec<&str> = cleaned.split(':').collect();
108 if parts.len() != 3 {
109 return Err(SteamIdError::InvalidFormat);
110 }
111
112 let account_id: u64 = parts[2].parse().map_err(SteamIdError::ParseError)?;
113
114 if account_id == 0 {
115 return Err(SteamIdError::InvalidFormat);
116 }
117
118 const STEAMID64_BASE: u64 = 76561197960265728;
119 let steam64 = STEAMID64_BASE + account_id;
120
121 Ok(steam64.to_string())
122 }
123 }
124}
125
126pub fn to_steam3(steam_id: &str) -> Result<String, SteamIdError> {
127 let incoming_format = parse_incoming_format(steam_id)?;
128 match incoming_format {
129 SteamIdKind::Steam64 => {
130 const STEAMID64_BASE: u64 = 76561197960265728;
131 let id64: u64 = steam_id.parse().map_err(SteamIdError::ParseError)?;
132 if id64 < STEAMID64_BASE {
133 return Err(SteamIdError::InvalidFormat);
134 }
135 let account_id = id64 - STEAMID64_BASE;
136 Ok(format!("[U:1:{}]", account_id))
137 }
138 SteamIdKind::Steam32 => {
139 let parts: Vec<&str> = steam_id.split(':').collect();
140 if parts.len() != 3 {
141 return Err(SteamIdError::InvalidFormat);
142 }
143 let y: u32 = parts[1].parse().map_err(SteamIdError::ParseError)?;
144 let z: u32 = parts[2].parse().map_err(SteamIdError::ParseError)?;
145
146 if y > 1 {
147 return Err(SteamIdError::InvalidFormat);
148 }
149 let account_id = z * 2 + y;
150 Ok(format!("[U:1:{}]", account_id))
151 }
152 SteamIdKind::Steam3 => Ok(steam_id.to_string()),
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum SteamIdError {
158 InvalidFormat,
159 InvalidPrefix,
160 InvalidY,
161 InvalidZ,
162 ParseError(ParseIntError),
163}
164
165impl std::fmt::Display for SteamIdError {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 match self {
168 Self::InvalidFormat => {
169 write!(f, "SteamID32 must have exactly 3 parts separated by ':'")
170 }
171 Self::InvalidPrefix => write!(f, "SteamID32 must start with 'STEAM_0' or 'STEAM_1'"),
172 Self::InvalidY => write!(f, "The second part (Y) must be 0 or 1"),
173 Self::InvalidZ => write!(f, "The third part (Z) must be a valid non-negative integer"),
174 Self::ParseError(e) => write!(f, "Number parsing failed: {}", e),
175 }
176 }
177}
178impl std::error::Error for SteamIdError {}
179
180impl From<ParseIntError> for SteamIdError {
181 fn from(err: ParseIntError) -> Self {
182 SteamIdError::ParseError(err)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 #[test]
190
191 fn test_to_steam32() {
192 assert_eq!(
193 to_steam32("STEAM_0:1:11110394"),
194 Ok("STEAM_0:1:11110394".to_string())
195 );
196 assert_eq!(
197 to_steam32("76561197982486517"),
198 Ok("STEAM_0:1:11110394".to_string())
199 );
200 assert_eq!(
201 to_steam32("[U:1:22220789]"),
202 Ok("STEAM_0:1:11110394".to_string())
203 );
204 }
205 #[test]
206 fn test_to_steam64() {
207 assert_eq!(
208 to_steam64("STEAM_0:1:11110394"),
209 Ok("76561197982486517".to_string())
210 );
211 assert_eq!(
212 to_steam64("76561197982486517"),
213 Ok("76561197982486517".to_string())
214 );
215 assert_eq!(
216 to_steam64("[U:1:22220789]"),
217 Ok("76561197982486517".to_string())
218 );
219 }
220
221 #[test]
222 fn test_to_steam3() {
223 assert_eq!(
224 to_steam3("[U:1:22220789]"),
225 Ok("[U:1:22220789]".to_string())
226 );
227 assert_eq!(
228 to_steam3("76561197982486517"),
229 Ok("[U:1:22220789]".to_string())
230 );
231 assert_eq!(
232 to_steam3("STEAM_0:1:11110394"),
233 Ok("[U:1:22220789]".to_string())
234 );
235 }
236 #[test]
237 fn test_parse_incoming_format() {
238 assert_eq!(
239 parse_incoming_format("76561197982486517"),
240 Ok(SteamIdKind::Steam64)
241 );
242 assert_eq!(
243 parse_incoming_format("[U:1:22220789]"),
244 Ok(SteamIdKind::Steam3)
245 );
246 assert_eq!(
247 parse_incoming_format("STEAM_0:1:11110394"),
248 Ok(SteamIdKind::Steam32)
249 );
250
251 assert_eq!(
252 parse_incoming_format("7656119798248"),
253 Err(SteamIdError::InvalidFormat)
254 );
255 assert_eq!(
256 parse_incoming_format("[STEAM_0:1:11110394]"),
257 Err(SteamIdError::InvalidFormat)
258 );
259 assert_eq!(
260 parse_incoming_format("U:1:22220789"),
261 Err(SteamIdError::InvalidFormat)
262 );
263 }
264}