use std::{fmt, ops::Deref, str::FromStr};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CsgoFriendCode(String);
impl CsgoFriendCode {
pub fn new(code: String) -> Self {
Self(code)
}
pub fn from_steam_id(steam_id: u64) -> Self {
let mut steam_id = steam_id;
let h = hash_steam_id(steam_id);
let mut r = 0u64;
for i in 0..8 {
let id_nibble = steam_id & 0xF;
steam_id >>= 4;
let hash_nibble = (h >> i) & 1;
let a = (r << 4) | id_nibble;
r = make_u64(r >> 28, a);
r = make_u64(r >> 31, (a << 1) | hash_nibble);
}
let mut res = b32(r);
if res.starts_with("AAAA-") {
res = res[5..].to_string();
}
Self(res)
}
pub fn to_account_id(&self) -> Option<u32> {
let code_clean = self.0.replace("-", "");
let full_code = match code_clean.len() {
13 => code_clean,
9 => format!("AAAA{}", code_clean),
_ => return None,
};
let mut val = b32_decode(&full_code)?;
val = val.swap_bytes();
let mut id: u32 = 0;
let mut hash_bits: u8 = 0;
for i in 0..8 {
let i_rev = 7 - i;
let block = val & 0x1F;
val >>= 5;
let id_nibble = (block >> 1) as u32;
let hash_bit = (block & 1) as u8;
id |= id_nibble << (i_rev * 4);
hash_bits |= hash_bit << i_rev;
}
let expected_hash = hash_steam_id(id as u64);
if (expected_hash & 0xFF) as u8 == hash_bits {
Some(id)
} else {
None
}
}
pub fn is_valid(&self) -> bool {
self.to_account_id().is_some()
}
}
impl Deref for CsgoFriendCode {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for CsgoFriendCode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim().to_uppercase();
let code = CsgoFriendCode(s);
if code.is_valid() {
Ok(code)
} else {
Err(())
}
}
}
impl From<u64> for CsgoFriendCode {
fn from(steam_id: u64) -> Self {
Self::from_steam_id(steam_id)
}
}
impl From<String> for CsgoFriendCode {
fn from(s: String) -> Self {
Self(s)
}
}
impl fmt::Display for CsgoFriendCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
fn make_u64(hi: u64, lo: u64) -> u64 {
(hi << 32) | lo
}
fn hash_steam_id(id: u64) -> u64 {
let account_id = id & 0xFFFFFFFF;
let strange_steam_id = account_id | 0x4353474F00000000;
let bytes = strange_steam_id.to_le_bytes();
let digest = md5::compute(bytes);
let slice: [u8; 4] = digest[0..4].try_into().expect("slice with incorrect length");
u32::from_le_bytes(slice) as u64
}
fn b32(input: u64) -> String {
let mut input = input;
let alnum = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let mut res = String::new();
input = input.swap_bytes();
for i in 0..13 {
if i == 4 || i == 9 {
res.push('-');
}
let index = (input & 0x1F) as usize;
res.push(alnum[index] as char);
input >>= 5;
}
res
}
fn b32_decode(input: &str) -> Option<u64> {
let alnum = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let mut res = 0u64;
let mut shift = 0;
for c in input.bytes() {
let pos = alnum.iter().position(|&x| x == c)?;
res |= (pos as u64) << shift;
shift += 5;
}
Some(res)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_steam_friend_code_serialization() {
let code = CsgoFriendCode::new("ABCDE-12345".to_string());
let json = serde_json::to_string(&code).unwrap();
assert_eq!(json, "\"ABCDE-12345\"");
let decoded: CsgoFriendCode = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, code);
}
#[test]
fn test_friend_code_conversion() {
let steam_id = 76561197960287930u64;
let code = CsgoFriendCode::from_steam_id(steam_id);
println!("Code: {}", code);
let decoded = code.to_account_id();
assert_eq!(decoded, Some(22202));
}
#[test]
fn test_roundtrip_random() {
let ids = vec![12345, 999999, 1, 0, 2147483647];
for id in ids {
let full_id = id as u64 | 0x0110000100000000;
let code = CsgoFriendCode::from_steam_id(full_id);
assert_eq!(code.to_account_id(), Some(id), "Failed for id {} code {}", id, code);
}
}
#[test]
fn test_invalid_code() {
let code = CsgoFriendCode("INVALID-CODE".to_string());
assert_eq!(code.to_account_id(), None);
}
#[test]
fn test_from_str() {
let id = 12345;
let full_id = id as u64 | 0x0110000100000000;
let code = CsgoFriendCode::from_steam_id(full_id);
let s = code.to_string();
let parsed: Result<CsgoFriendCode, _> = s.parse();
assert!(parsed.is_ok());
assert_eq!(parsed.unwrap().to_account_id(), Some(id));
assert!(CsgoFriendCode::from_str("INVALID").is_err());
}
}