use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, VecDeque}, sync::{Arc, RwLock}, time::{SystemTime, UNIX_EPOCH}, }; /// Лимит одного передаваемого медиафайла (200 МБ) pub const MAX_PAYLOAD_SIZE: usize = 200 * 1024 * 1024; /// Максимум текстовых сообщений, хранимых в ОЗУ pub const MAX_RAM_TEXTS: usize = 50; /// Папка на диске для сохранения входящего медиаконтента pub const MEDIA_DIR: &str = "received_media"; /// Тип сервиса для обнаружения через mDNS pub const SERVICE_TYPE: &str = "_ramshare._tcp.local."; /// Текстовое сообщение (хранится ТОЛЬКО в ОЗУ) #[derive(Clone, Serialize, Deserialize)] pub struct TextItem { pub sender: String, pub text: String, pub timestamp: u64, } /// Метаданные медиафайла (сам файл сохраняется НА ДИСК) #[derive(Clone, Serialize, Deserialize)] pub struct MediaItem { pub sender: String, pub filename: String, pub saved_path: String, pub mime: String, pub size: usize, pub timestamp: u64, } /// Устройство в локальной сети, найденное через mDNS #[derive(Clone, Serialize, Deserialize)] pub struct PeerNode { pub name: String, pub ip: String, pub port: u16, pub fingerprint: String, pub pin_code: String, pub verified: bool, } /// Глобальное состояние приложения pub struct AppState { pub device_name: String, pub my_ip: String, pub p2p_port: u16, pub my_fingerprint: String, pub ram_texts: RwLock>, pub disk_media: RwLock>, pub peers: RwLock>, pub trusted_fingerprints: Arc>>, pub http_client: reqwest::Client, } #[derive(Serialize, Deserialize)] pub struct IncomingTextPayload { pub sender: String, pub text: String, } #[derive(Deserialize)] pub struct UiSendTextRequest { pub target_ip: String, pub text: String, } #[derive(Deserialize)] pub struct UiTrustRequest { pub target_ip: String, pub trust: bool, } pub fn now_sec() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() } /// Декодирование URL-encoded имени файла (для поддержки кириллицы и пробелов) pub fn url_decode(input: &str) -> String { let mut bytes = Vec::with_capacity(input.len()); let mut chars = input.as_bytes().iter().copied(); while let Some(b) = chars.next() { if b == b'%' { if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) { if let Ok(decoded) = u8::from_str_radix(&format!("{}{}", c1 as char, c2 as char), 16) { bytes.push(decoded); continue; } } } else if b == b'+' { bytes.push(b' '); } else { bytes.push(b); } } String::from_utf8_lossy(&bytes).into_owned() } /// Защита от Path Traversal при сохранении медиа на диск pub fn sanitize_filename(name: &str) -> String { let decoded = url_decode(name); let clean: String = decoded .chars() .map(|c| match c { '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '_', _ => c, }) .collect(); let trimmed = clean.trim_matches('.').trim(); if trimmed.is_empty() { "media_file.bin".to_string() } else { trimmed.to_string() } }