use std::{collections::HashMap, sync::mpsc::Sender};
use crate::{error::Result, message::Message, utils::zlib::deflate};
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct File {
pub username: String,
pub name: String,
pub size: u64,
pub attribs: HashMap<u32, u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClientVersion {
pub major: u32,
pub minor: u32,
}
impl ClientVersion {
pub const MAJOR: u32 = 176;
pub const REFERENCE_CLIENT: Self = Self {
major: Self::MAJOR,
minor: 100,
};
}
impl Default for ClientVersion {
fn default() -> Self {
Self {
major: Self::MAJOR,
minor: 1,
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct SearchResult {
pub token: u32,
pub files: Vec<File>,
pub slots: u8,
pub speed: u32,
pub username: String,
}
#[derive(Debug, Clone)]
pub struct Search {
pub token: u32,
pub results: Vec<SearchResult>,
}
impl SearchResult {
pub fn new_from_message(message: &mut Message) -> Result<Self> {
let pointer = message.get_pointer();
let size = message.get_size();
let data: Vec<u8> = message.get_slice(pointer, size);
let deflated = deflate(&data)?;
let mut message = Message::new_with_data(deflated);
let username = message.read_string();
let token = message.read_int32();
let n_files = message.read_int32();
let mut files: Vec<File> = Vec::new();
for _ in 0..n_files {
if message.get_pointer() >= message.get_size() {
break;
}
message.read_int8();
let name = message.read_string();
let size = message.read_int64();
message.read_string();
let n_attribs = message.read_int32();
let mut attribs: HashMap<u32, u32> = HashMap::new();
for _ in 0..n_attribs {
if message.get_pointer() + 8 > message.get_size() {
break;
}
attribs.insert(message.read_int32(), message.read_int32());
}
files.push(File {
username: username.clone(),
name,
size,
attribs,
});
}
let slots = message.read_int8();
let speed = message.read_int32();
Ok(Self {
token,
files,
slots,
speed,
username,
})
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Transfer {
pub direction: u32,
pub token: u32,
pub filename: String,
pub size: u64,
}
#[derive(Debug, Clone, Default)]
pub struct DownloadMetadata {
pub bitrate: Option<u32>,
pub length_seconds: Option<u32>,
pub peer_upload_speed: Option<u32>,
pub peer_free_slots: Option<u8>,
}
#[derive(Debug, Clone)]
pub struct Download {
pub username: String,
pub filename: String,
pub token: u32,
pub size: u64,
pub download_directory: String,
pub status: DownloadStatus,
pub sender: Sender<DownloadStatus>,
pub queue_position: Option<u32>,
pub metadata: DownloadMetadata,
}
impl Download {
#[must_use]
pub const fn is_finished(&self) -> bool {
matches!(
self.status,
DownloadStatus::Completed
| DownloadStatus::Failed(_)
| DownloadStatus::TimedOut
)
}
#[must_use]
pub const fn bytes_downloaded(&self) -> u64 {
match &self.status {
DownloadStatus::InProgress {
bytes_downloaded, ..
}
| DownloadStatus::Paused {
bytes_downloaded, ..
} => *bytes_downloaded,
DownloadStatus::Completed => self.size,
_ => 0,
}
}
#[must_use]
pub const fn speed_bytes_per_sec(&self) -> f64 {
match &self.status {
DownloadStatus::InProgress {
speed_bytes_per_sec,
..
} => *speed_bytes_per_sec,
_ => 0.0,
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum DownloadStatus {
Queued,
InProgress {
bytes_downloaded: u64,
total_bytes: u64,
speed_bytes_per_sec: f64,
},
Paused {
bytes_downloaded: u64,
total_bytes: u64,
},
Completed,
Failed(Option<String>),
TimedOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UserStatus {
#[default]
Offline,
Away,
Online,
}
impl UserStatus {
#[must_use]
pub const fn from_code(code: u32) -> Self {
match code {
1 => Self::Away,
2 => Self::Online,
_ => Self::Offline,
}
}
#[must_use]
pub const fn is_reachable(self) -> bool {
matches!(self, Self::Away | Self::Online)
}
}
impl std::fmt::Display for UserStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Offline => "offline",
Self::Away => "away",
Self::Online => "online",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UserPresence {
pub status: UserStatus,
pub privileged: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct UserStats {
pub average_speed: u32,
pub shared_files: u32,
pub shared_folders: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UserInfo {
pub username: String,
pub presence: Option<UserPresence>,
pub stats: Option<UserStats>,
}
impl UserInfo {
#[must_use]
pub const fn pending(username: String) -> Self {
Self {
username,
presence: None,
stats: None,
}
}
#[must_use]
pub const fn is_complete(&self) -> bool {
self.presence.is_some() && self.stats.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoomInfo {
pub name: String,
pub user_count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoomEvent {
List(Vec<RoomInfo>),
Joined { room: String, users: Vec<String> },
Left { room: String },
Message {
room: String,
username: String,
message: String,
},
UserJoined { room: String, username: String },
UserLeft { room: String, username: String },
}
impl Transfer {
pub fn new_from_message(message: &mut Message) -> Self {
let direction = message.read_int32();
let token = message.read_int32();
let filename = message.read_string();
let size = message.read_int64();
Self {
direction,
token,
filename,
size,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn search_result_hostile_file_count_does_not_hang() {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&7u32.to_le_bytes()); body.extend_from_slice(&u32::MAX.to_le_bytes()); let compressed = crate::utils::zlib::compress_stored(&body);
let mut message = Message::new_with_data(compressed);
let result = SearchResult::new_from_message(&mut message)
.expect("hostile count should parse, not error");
assert_eq!(result.token, 7);
assert!(result.files.is_empty());
}
#[test]
fn transfer_new_from_truncated_message_does_not_panic() {
let mut message = Message::new_with_data(vec![1, 0, 0]);
let transfer = Transfer::new_from_message(&mut message);
assert_eq!(transfer.direction, 0);
assert_eq!(transfer.token, 0);
assert_eq!(transfer.filename, "");
assert_eq!(transfer.size, 0);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UploadStatus {
Queued(u32),
InProgress,
Completed,
Cancelled,
Failed(String),
}
#[derive(Debug, Clone)]
pub struct UploadInfo {
pub username: String,
pub filename: String,
pub size: u64,
pub bytes_sent: u64,
pub status: UploadStatus,
pub speed_bytes_per_sec: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SessionLoss {
Displaced = 1,
Disconnected = 2,
}
impl std::fmt::Display for SessionLoss {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Displaced => f.write_str(
"another login took over this username; the server allows one \
session per account",
),
Self::Disconnected => {
f.write_str("the connection to the server dropped")
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SessionWatch(std::sync::Arc<std::sync::atomic::AtomicU8>);
impl SessionWatch {
const LIVE: u8 = 0;
pub fn record(&self, loss: SessionLoss) {
let _ = self.0.compare_exchange(
Self::LIVE,
loss as u8,
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
);
}
#[must_use]
pub fn loss(&self) -> Option<SessionLoss> {
match self.0.load(std::sync::atomic::Ordering::Relaxed) {
1 => Some(SessionLoss::Displaced),
2 => Some(SessionLoss::Disconnected),
_ => None,
}
}
}