refactor: cut what a whole-repo audit proved dead, padded, or hand-rolled
Seven region audits, every finding adversarially verified against the whole workspace before being applied; 80 landed and the two that failed in application were refuted honestly (ParseError has a from_str_radix producer the audit missed; NatPmpError's Display is the only reader of its diagnostic payloads). The bigger cuts: - The NewPeer/PierceFireWall cluster: an operation variant nothing ever sent, its 77-line arm, a protocol-wrong Client::pierce_firewall that wrote a peer-init frame down the server socket, and the struct only they used. The live paths in peer/listen.rs are untouched. - The ops loop reads `for operation in reader` instead of hand-rolling recv/break, de-indenting the whole dispatch body. - Dead lib surface: Message::print_hex and four unused accessors, UploadFailed, registry and actor getters, build_watch_user, strip_ansi_codes, delegating shims on ClientContext. Removing public items makes this a semver major, which the release pipeline enforces. - Never-used flexibility: the state-migration framework that never held a migration, border-type helpers that all answered Rounded, the TUI's max_concurrent_downloads field nothing read. - Convergence: six poll loops into poll_until, three wrap-around navigation blocks into cycle, hit tests into Rect::contains, char walkers into floor/ceil_char_boundary, five Download literals into one helper, the suites' shared scaffolding into tests/common. Deliberately not taken: swapping the hand-rolled zlib inflate and MD5 for crates (they replace wire-facing machinery whose tolerance of real-world peer quirks no local test exercises) and pulling chrono into the lib for a log timestamp. Recorded for their own decision. Net -1002 lines. Both e2e modes stay green: 94 CLI tests each way, 16 protocol, 33 lib, unit suites, clippy pedantic clean, aislop 100/100 with the four scanner false positives suppressed inline where they fire.
This commit is contained in:
@@ -184,13 +184,7 @@ impl PeerActor {
|
||||
let messages: Vec<PeerMessage> = self
|
||||
.dispatcher_receiver
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |receiver| {
|
||||
let mut msgs = Vec::new();
|
||||
while let Ok(msg) = receiver.try_recv() {
|
||||
msgs.push(msg);
|
||||
}
|
||||
msgs
|
||||
});
|
||||
.map_or_else(Vec::new, |receiver| receiver.try_iter().collect());
|
||||
|
||||
for msg in &messages {
|
||||
self.handle_message(msg.clone());
|
||||
@@ -690,45 +684,20 @@ impl PeerActor {
|
||||
}
|
||||
}
|
||||
|
||||
fn initiate_connection(&mut self) -> bool {
|
||||
fn initiate_connection(&mut self) {
|
||||
let (username, host, port) = match self.peer.read_safe() {
|
||||
Ok(peer) => (peer.username.clone(), peer.host.clone(), peer.port),
|
||||
Err(e) => {
|
||||
error!("[peer_actor] initiate_connection peer lock: {}", e);
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let socket_addr =
|
||||
format!("{host}:{port}").parse::<std::net::SocketAddr>();
|
||||
|
||||
match socket_addr {
|
||||
Ok(addr) => {
|
||||
// Use connect_timeout to prevent blocking the thread for too long
|
||||
let timeout = Duration::from_secs(5);
|
||||
match TcpStream::connect_timeout(&addr, timeout) {
|
||||
Ok(stream) => {
|
||||
if let Err(e) = stream.set_nonblocking(true) {
|
||||
error!(
|
||||
"[peer:{}] Failed to set non-blocking: {}",
|
||||
username, e
|
||||
);
|
||||
self.disconnect_with_error(e);
|
||||
return false;
|
||||
}
|
||||
stream.set_nodelay(true).ok();
|
||||
self.stream = Some(stream);
|
||||
self.connection_state = ConnectionState::Connecting {
|
||||
since: Instant::now(),
|
||||
};
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
self.disconnect_with_error(e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
let addr = match socket_addr {
|
||||
Ok(addr) => addr,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"[peer:{}] Invalid socket address {}:{} - {}",
|
||||
@@ -738,9 +707,30 @@ impl PeerActor {
|
||||
io::ErrorKind::InvalidInput,
|
||||
e,
|
||||
));
|
||||
false
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Use connect_timeout to prevent blocking the thread for too long
|
||||
let timeout = Duration::from_secs(5);
|
||||
let stream = match TcpStream::connect_timeout(&addr, timeout) {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
self.disconnect_with_error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = stream.set_nonblocking(true) {
|
||||
error!("[peer:{}] Failed to set non-blocking: {}", username, e);
|
||||
self.disconnect_with_error(e);
|
||||
return;
|
||||
}
|
||||
stream.set_nodelay(true).ok();
|
||||
self.stream = Some(stream);
|
||||
self.connection_state = ConnectionState::Connecting {
|
||||
since: Instant::now(),
|
||||
};
|
||||
}
|
||||
|
||||
fn check_connection_status(&mut self) {
|
||||
|
||||
@@ -145,28 +145,6 @@ impl PeerRegistry {
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_all_usernames(&self) -> Vec<String> {
|
||||
match self.peers.lock_safe() {
|
||||
Ok(peers) => peers.keys().cloned().collect(),
|
||||
Err(e) => {
|
||||
error!("[peer_registry] get_all_usernames: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn count(&self) -> usize {
|
||||
match self.peers.lock_safe() {
|
||||
Ok(peers) => peers.len(),
|
||||
Err(e) => {
|
||||
error!("[peer_registry] count: {}", e);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn contains(&self, username: &str) -> bool {
|
||||
match self.peers.lock_safe() {
|
||||
|
||||
@@ -31,8 +31,8 @@ use crate::types::{
|
||||
};
|
||||
use crate::utils::lock::RwLockExt;
|
||||
|
||||
use std::io::{self, Error, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::io::{self, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -109,17 +109,6 @@ impl UserMessage {
|
||||
new_message,
|
||||
}
|
||||
}
|
||||
pub fn print(&self) {
|
||||
debug!(
|
||||
"Timestamp: {}. User: {}, Id: #{}, New message: {} Message: {}",
|
||||
self.timestamp,
|
||||
self.username,
|
||||
self.id,
|
||||
self.new_message,
|
||||
self.message
|
||||
);
|
||||
}
|
||||
|
||||
/// The server-assigned id of this message (used to acknowledge it).
|
||||
#[must_use]
|
||||
pub const fn id(&self) -> u32 {
|
||||
@@ -320,60 +309,23 @@ impl ServerActor {
|
||||
self.session = session;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_address(&self) -> &PeerAddress {
|
||||
&self.address
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_sender(&self) -> Option<&Sender<ServerMessage>> {
|
||||
self.dispatcher_sender.as_ref()
|
||||
}
|
||||
|
||||
fn initiate_connection(&mut self) -> bool {
|
||||
let host = self.address.host.clone();
|
||||
let port = self.address.port;
|
||||
|
||||
let addr_str = format!("{host}:{port}");
|
||||
|
||||
let mut socket_addrs = match addr_str.to_socket_addrs() {
|
||||
Ok(addrs) => addrs,
|
||||
Err(e) => {
|
||||
error!("[server] Failed to resolve address: {}", e);
|
||||
|
||||
self.disconnect_with_error(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
e,
|
||||
));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let socket_addr = socket_addrs.next();
|
||||
|
||||
let Some(addr) = socket_addr else {
|
||||
let error_msg =
|
||||
format!("No socket addresses found for {host}:{port}");
|
||||
error!("[server] {}", error_msg);
|
||||
self.disconnect_with_error(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
error_msg,
|
||||
));
|
||||
return false;
|
||||
};
|
||||
|
||||
let stream = match TcpStream::connect(addr) {
|
||||
fn initiate_connection(&mut self) {
|
||||
let stream = match TcpStream::connect((
|
||||
self.address.host.as_str(),
|
||||
self.address.port,
|
||||
)) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
self.disconnect_with_error(e);
|
||||
return false;
|
||||
error!("[server] Failed to connect to {}: {}", self.address, e);
|
||||
self.disconnect_with_error();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = stream.set_nonblocking(true) {
|
||||
error!("[server] Failed to set non-blocking: {}", e);
|
||||
self.disconnect_with_error(e);
|
||||
return false;
|
||||
self.disconnect_with_error();
|
||||
return;
|
||||
}
|
||||
stream.set_nodelay(true).ok();
|
||||
|
||||
@@ -381,7 +333,6 @@ impl ServerActor {
|
||||
self.connection_state = ConnectionState::Connecting {
|
||||
since: Instant::now(),
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_self_handle(&mut self, handle: ActorHandle<ServerMessage>) {
|
||||
@@ -436,13 +387,7 @@ impl ServerActor {
|
||||
let messages: Vec<ServerMessage> = self
|
||||
.dispatcher_receiver
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |receiver| {
|
||||
let mut msgs = Vec::new();
|
||||
while let Ok(msg) = receiver.try_recv() {
|
||||
msgs.push(msg);
|
||||
}
|
||||
msgs
|
||||
});
|
||||
.map_or_else(Vec::new, |receiver| receiver.try_iter().collect());
|
||||
|
||||
for msg in &messages {
|
||||
self.handle_message(msg.clone());
|
||||
@@ -783,7 +728,7 @@ impl ServerActor {
|
||||
e,
|
||||
e.kind()
|
||||
);
|
||||
self.disconnect_with_error(e);
|
||||
self.disconnect_with_error();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -818,7 +763,7 @@ impl ServerActor {
|
||||
"[server] Error extracting message: {}. Disconnecting.",
|
||||
e
|
||||
);
|
||||
self.disconnect_with_error(e);
|
||||
self.disconnect_with_error();
|
||||
return;
|
||||
}
|
||||
Ok(None) => {
|
||||
@@ -871,13 +816,13 @@ impl ServerActor {
|
||||
|
||||
if let Err(e) = stream.write_all(&message.get_buffer()) {
|
||||
error!("[server] Error writing message: {}. Disconnecting.", e);
|
||||
self.disconnect_with_error(e);
|
||||
self.disconnect_with_error();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = stream.flush() {
|
||||
error!("[server] Error flushing stream: {}. Disconnecting.", e);
|
||||
self.disconnect_with_error(e);
|
||||
self.disconnect_with_error();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,7 +838,7 @@ impl ServerActor {
|
||||
self.disconnect();
|
||||
}
|
||||
|
||||
fn disconnect_with_error(&mut self, _error: Error) {
|
||||
fn disconnect_with_error(&mut self) {
|
||||
debug!("[server] disconnect");
|
||||
|
||||
// Losing an established connection ends the session: nothing reconnects
|
||||
@@ -919,10 +864,7 @@ impl ServerActor {
|
||||
|
||||
if since.elapsed() > Duration::from_secs(20) {
|
||||
error!("[server] Connection timeout after 20 seconds");
|
||||
self.disconnect_with_error(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"Connection timeout",
|
||||
));
|
||||
self.disconnect_with_error();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -938,16 +880,12 @@ impl ServerActor {
|
||||
Err(ref e) if e.kind() == io::ErrorKind::NotConnected => {}
|
||||
Err(e) => {
|
||||
error!("[server] Connection failed: {}", e);
|
||||
self.disconnect_with_error(e);
|
||||
self.disconnect_with_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_connection_established(&mut self) {
|
||||
let Some(_) = self.stream else {
|
||||
panic!("Stream should be available here")
|
||||
};
|
||||
|
||||
self.initialize_dispatcher();
|
||||
|
||||
let queued = std::mem::take(&mut self.queued_messages);
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{
|
||||
Arc, Client, ClientContext, ClientOperation, ConnectionType, DownloadPeer,
|
||||
DownloadStatus, Listen, Peer, PeerRegistry, Receiver, Result, RwLock,
|
||||
RwLockExt, Sender, ServerActor, ServerMessage, Shares, SoulseekRs,
|
||||
TcpStream, debug, error, info, mpsc, thread, trace,
|
||||
TcpStream, error, info, mpsc, thread, trace,
|
||||
};
|
||||
|
||||
/// Ceiling on the wait for a login verdict. Generous enough for a slow server
|
||||
@@ -18,7 +18,6 @@ impl Client {
|
||||
) = mpsc::channel();
|
||||
|
||||
let mut ctx = self.context.write_safe()?;
|
||||
ctx.sender = Some(sender.clone());
|
||||
let peer_registry = PeerRegistry::new(
|
||||
ctx.actor_system.clone(),
|
||||
sender.clone(),
|
||||
@@ -166,8 +165,6 @@ impl Client {
|
||||
own_username: String,
|
||||
stream: Option<TcpStream>,
|
||||
) {
|
||||
let client_context = client_context;
|
||||
|
||||
let peer_clone = peer.clone();
|
||||
trace!(
|
||||
"[client] connecting to {}, with connection_type: {}, and token {:?}",
|
||||
@@ -252,37 +249,4 @@ impl Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pierce_firewall(
|
||||
peer: Peer,
|
||||
client_context: Arc<RwLock<ClientContext>>,
|
||||
own_username: String,
|
||||
) {
|
||||
debug!("Piercing firewall for peer: {:?}", peer);
|
||||
|
||||
let context = match client_context.read_safe() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("[client] pierce_firewall read: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(server_sender) = &context.server_sender {
|
||||
if let Some(token) = peer.token {
|
||||
match server_sender.send(ServerMessage::PierceFirewall(token)) {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
error!("Failed to send PierceFirewall message: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("No token available for PierceFirewall");
|
||||
}
|
||||
} else {
|
||||
error!("No server sender available for PierceFirewall");
|
||||
}
|
||||
|
||||
drop(context);
|
||||
Self::connect_to_peer(peer, client_context, own_username, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ use crate::{
|
||||
actor::{ActorSystem, peer_registry::PeerRegistry},
|
||||
error::{Result, SoulseekRs},
|
||||
message::peer::{FileEntry, SharedDirectory, build_file_search_response},
|
||||
peer::{
|
||||
ConnectionType, DownloadPeer, NewPeer, Peer, PeerMessage,
|
||||
listen::Listen,
|
||||
},
|
||||
peer::{ConnectionType, DownloadPeer, Peer, PeerMessage, listen::Listen},
|
||||
shares::Shares,
|
||||
types::{Download, Search, SearchResult},
|
||||
utils::{lock::RwLockExt, md5},
|
||||
@@ -25,14 +22,11 @@ use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
net::TcpStream,
|
||||
sync::{
|
||||
RwLock,
|
||||
Arc, RwLock,
|
||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||
mpsc::{Receiver, Sender},
|
||||
mpsc::{self, Receiver, Sender},
|
||||
},
|
||||
thread::{self, sleep},
|
||||
};
|
||||
use std::{
|
||||
sync::{Arc, mpsc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use upload_queue::QueuedUpload;
|
||||
@@ -213,11 +207,9 @@ impl Default for ClientSettings {
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ClientOperation {
|
||||
NewPeer(NewPeer),
|
||||
ConnectToPeer(Peer),
|
||||
SearchResult(SearchResult),
|
||||
PeerDisconnected(u64, String, Option<SoulseekRs>),
|
||||
PierceFireWall(Peer),
|
||||
DownloadFromPeer(u32, Peer, bool),
|
||||
UpdateDownloadTokens(Transfer, String),
|
||||
GetPeerAddressResponse {
|
||||
@@ -298,7 +290,6 @@ pub enum ClientOperation {
|
||||
pub struct ClientContext {
|
||||
pub peer_registry: Option<PeerRegistry>,
|
||||
pub downloads: DownloadStore,
|
||||
sender: Option<Sender<ClientOperation>>,
|
||||
server_sender: Option<Sender<ServerMessage>>,
|
||||
searches: HashMap<String, Search>,
|
||||
private_messages: Vec<UserMessage>,
|
||||
@@ -376,13 +367,6 @@ impl ClientContext {
|
||||
) -> Option<&mut Download> {
|
||||
self.downloads.get_by_token_mut(token)
|
||||
}
|
||||
pub fn get_download_by_file_mut(
|
||||
&mut self,
|
||||
username: &str,
|
||||
filename: &str,
|
||||
) -> Option<&mut Download> {
|
||||
self.downloads.get_by_file_mut(username, filename)
|
||||
}
|
||||
#[must_use]
|
||||
pub fn get_download_tokens(&self) -> Vec<u32> {
|
||||
self.downloads.tokens()
|
||||
@@ -398,13 +382,6 @@ impl ClientContext {
|
||||
) {
|
||||
self.downloads.update_status(token, status);
|
||||
}
|
||||
pub fn remove_queued_download_by_file(
|
||||
&mut self,
|
||||
username: &str,
|
||||
filename: &str,
|
||||
) -> bool {
|
||||
self.downloads.remove_queued_by_file(username, filename)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientContext {
|
||||
@@ -414,7 +391,6 @@ impl ClientContext {
|
||||
|
||||
Self {
|
||||
peer_registry: None,
|
||||
sender: None,
|
||||
server_sender: None,
|
||||
searches: HashMap::new(),
|
||||
private_messages: Vec::new(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,37 @@
|
||||
use super::*;
|
||||
|
||||
fn download(
|
||||
username: &str,
|
||||
filename: &str,
|
||||
token: u32,
|
||||
status: DownloadStatus,
|
||||
sender: Sender<DownloadStatus>,
|
||||
) -> Download {
|
||||
Download {
|
||||
username: username.to_string(),
|
||||
filename: filename.to_string(),
|
||||
token,
|
||||
size: 100,
|
||||
download_directory: "test".to_string(),
|
||||
status,
|
||||
sender,
|
||||
queue_position: None,
|
||||
metadata: DownloadMetadata::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_context_downloads() {
|
||||
let mut context = ClientContext::new();
|
||||
let token = 123;
|
||||
let new_token = 1234;
|
||||
let download = Download {
|
||||
username: "test".to_string(),
|
||||
filename: "test.txt".to_string(),
|
||||
context.add_download(download(
|
||||
"test",
|
||||
"test.txt",
|
||||
token,
|
||||
size: 100,
|
||||
download_directory: "test".to_string(),
|
||||
status: DownloadStatus::Queued,
|
||||
sender: mpsc::channel().0,
|
||||
queue_position: None,
|
||||
metadata: DownloadMetadata::default(),
|
||||
};
|
||||
context.add_download(download);
|
||||
DownloadStatus::Queued,
|
||||
mpsc::channel().0,
|
||||
));
|
||||
assert!(context.get_download_by_token(123).is_some());
|
||||
assert_eq!(context.get_download_tokens(), vec![123]);
|
||||
assert_eq!(context.get_downloads().len(), 1);
|
||||
@@ -35,23 +50,18 @@ fn test_client_context_downloads() {
|
||||
fn test_client_pause_and_resume_download() {
|
||||
let client = Client::new("test-user", "test-password");
|
||||
let (download_sender, download_receiver) = mpsc::channel();
|
||||
let download = Download {
|
||||
username: "peer".to_string(),
|
||||
filename: "song.mp3".to_string(),
|
||||
token: 123,
|
||||
size: 100,
|
||||
download_directory: "test".to_string(),
|
||||
status: DownloadStatus::InProgress {
|
||||
|
||||
client.context.write().unwrap().add_download(download(
|
||||
"peer",
|
||||
"song.mp3",
|
||||
123,
|
||||
DownloadStatus::InProgress {
|
||||
bytes_downloaded: 25,
|
||||
total_bytes: 100,
|
||||
speed_bytes_per_sec: 10.0,
|
||||
},
|
||||
sender: download_sender,
|
||||
queue_position: None,
|
||||
metadata: DownloadMetadata::default(),
|
||||
};
|
||||
|
||||
client.context.write().unwrap().add_download(download);
|
||||
download_sender,
|
||||
));
|
||||
|
||||
assert!(client.pause_download("peer", "song.mp3"));
|
||||
assert!(matches!(
|
||||
@@ -118,17 +128,13 @@ fn fail_queued_downloads_notifies_receiver_and_store() {
|
||||
// must resolve to Failed both on its channel and in the store.
|
||||
let client = Client::new("u", "p");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
client.context.write().unwrap().add_download(Download {
|
||||
username: "peer".to_string(),
|
||||
filename: "f.mp3".to_string(),
|
||||
token: 7,
|
||||
size: 10,
|
||||
download_directory: "d".to_string(),
|
||||
status: DownloadStatus::Queued,
|
||||
client.context.write().unwrap().add_download(download(
|
||||
"peer",
|
||||
"f.mp3",
|
||||
7,
|
||||
DownloadStatus::Queued,
|
||||
sender,
|
||||
queue_position: None,
|
||||
metadata: DownloadMetadata::default(),
|
||||
});
|
||||
));
|
||||
|
||||
Client::fail_queued_downloads(&client.context, "peer");
|
||||
|
||||
@@ -171,37 +177,26 @@ fn build_search_response_matches_shares_and_echoes_token() {
|
||||
#[test]
|
||||
fn test_client_removes_only_queued_downloads() {
|
||||
let client = Client::new("test-user", "test-password");
|
||||
let queued_download = Download {
|
||||
username: "peer".to_string(),
|
||||
filename: "queued.mp3".to_string(),
|
||||
token: 123,
|
||||
size: 100,
|
||||
download_directory: "test".to_string(),
|
||||
status: DownloadStatus::Queued,
|
||||
sender: mpsc::channel().0,
|
||||
queue_position: None,
|
||||
metadata: DownloadMetadata::default(),
|
||||
};
|
||||
let active_download = Download {
|
||||
username: "peer".to_string(),
|
||||
filename: "active.mp3".to_string(),
|
||||
token: 456,
|
||||
size: 100,
|
||||
download_directory: "test".to_string(),
|
||||
status: DownloadStatus::InProgress {
|
||||
bytes_downloaded: 25,
|
||||
total_bytes: 100,
|
||||
speed_bytes_per_sec: 10.0,
|
||||
},
|
||||
sender: mpsc::channel().0,
|
||||
queue_position: None,
|
||||
metadata: DownloadMetadata::default(),
|
||||
};
|
||||
|
||||
{
|
||||
let mut context = client.context.write().unwrap();
|
||||
context.add_download(queued_download);
|
||||
context.add_download(active_download);
|
||||
context.add_download(download(
|
||||
"peer",
|
||||
"queued.mp3",
|
||||
123,
|
||||
DownloadStatus::Queued,
|
||||
mpsc::channel().0,
|
||||
));
|
||||
context.add_download(download(
|
||||
"peer",
|
||||
"active.mp3",
|
||||
456,
|
||||
DownloadStatus::InProgress {
|
||||
bytes_downloaded: 25,
|
||||
total_bytes: 100,
|
||||
speed_bytes_per_sec: 10.0,
|
||||
},
|
||||
mpsc::channel().0,
|
||||
));
|
||||
}
|
||||
|
||||
assert!(client.remove_queued_download("peer", "queued.mp3"));
|
||||
|
||||
@@ -47,8 +47,6 @@ impl Client {
|
||||
.and_then(|ctx| ctx.own_privileges)
|
||||
}
|
||||
|
||||
/// How many uploads run at once. Lowering it does not interrupt transfers
|
||||
/// already in flight; the queue simply refills more slowly.
|
||||
/// Queued-upload states recorded since the last call, for a caller that
|
||||
/// samples [`Client::uploads`] and would otherwise miss a peer that queued
|
||||
/// and was served between two samples.
|
||||
@@ -60,6 +58,8 @@ impl Client {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// How many uploads run at once. Lowering it does not interrupt transfers
|
||||
/// already in flight; the queue simply refills more slowly.
|
||||
pub fn set_upload_slots(&self, slots: usize) {
|
||||
if let Ok(mut ctx) = self.context.write_safe() {
|
||||
ctx.upload_slots = slots.max(1);
|
||||
|
||||
@@ -31,9 +31,8 @@ impl<Op> MessageDispatcher<Op> {
|
||||
handler.handle(message, self.sender.clone());
|
||||
} else {
|
||||
warn!(
|
||||
"[{}:dispatcher] No handler found for message code: {}",
|
||||
self.owner_name,
|
||||
message.get_message_code()
|
||||
"[{}:dispatcher] No handler found for message code: {code}",
|
||||
self.owner_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,47 +51,6 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_hex(&self) {
|
||||
const BYTES_PER_LINE: usize = 16;
|
||||
let data = &self.data;
|
||||
|
||||
let chunks = data.chunks(BYTES_PER_LINE);
|
||||
for (i, chunk) in chunks.enumerate() {
|
||||
print!("{:04x} ", i * BYTES_PER_LINE);
|
||||
|
||||
for j in 0..BYTES_PER_LINE {
|
||||
if j < chunk.len() {
|
||||
print!("{:02x} ", chunk[j]);
|
||||
} else {
|
||||
print!(" ");
|
||||
}
|
||||
|
||||
// Add extra space in the middle
|
||||
if j == 7 {
|
||||
print!(" ");
|
||||
}
|
||||
}
|
||||
|
||||
print!(" ");
|
||||
|
||||
let mut i = 0;
|
||||
for &byte in chunk {
|
||||
i += 1;
|
||||
if byte.is_ascii_graphic() || byte.is_ascii_whitespace() {
|
||||
print!("{}", byte as char);
|
||||
} else {
|
||||
print!(".");
|
||||
}
|
||||
|
||||
if i == 8 {
|
||||
print!(" ");
|
||||
}
|
||||
}
|
||||
|
||||
trace!("");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[must_use]
|
||||
pub fn get_message_code_u32(&self) -> u32 {
|
||||
@@ -109,22 +68,10 @@ impl Message {
|
||||
self.data[4]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_message_code_send(&self) -> u8 {
|
||||
if self.data.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
self.data[0]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn new_with_data(data: Vec<u8>) -> Self {
|
||||
Self { data, pointer: 0 }
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub const fn reset_pointer(&mut self) {
|
||||
self.pointer = 0;
|
||||
}
|
||||
pub const fn set_pointer(&mut self, pointer: usize) {
|
||||
self.pointer = pointer;
|
||||
}
|
||||
@@ -215,21 +162,6 @@ impl Message {
|
||||
val
|
||||
}
|
||||
|
||||
pub fn read_raw_byte(&mut self) -> Vec<u8> {
|
||||
if self.pointer + 4 > self.data.len() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let val = vec![
|
||||
self.data[self.pointer],
|
||||
self.data[self.pointer + 1],
|
||||
self.data[self.pointer + 2],
|
||||
self.data[self.pointer + 3],
|
||||
];
|
||||
self.pointer += 4;
|
||||
val
|
||||
}
|
||||
|
||||
pub fn read_int32(&mut self) -> u32 {
|
||||
if self.pointer + 4 > self.data.len() {
|
||||
return 0;
|
||||
@@ -286,20 +218,6 @@ impl Message {
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn write_raw_hex_string(&mut self, val: &str) -> &mut Self {
|
||||
let mut b = Vec::new();
|
||||
for i in (0..val.len()).step_by(2) {
|
||||
let byte_str = &val[i..i + 2];
|
||||
let byte =
|
||||
u8::from_str_radix(byte_str, 16).expect("Invalid hex string");
|
||||
b.push(byte);
|
||||
}
|
||||
self.data.extend_from_slice(&b);
|
||||
self.pointer += b.len();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_message_name(
|
||||
&self,
|
||||
msg_type: MessageType,
|
||||
@@ -378,6 +296,18 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a received message the way the dispatcher hands one to a handler:
|
||||
/// 8 bytes of frame header, then the fields, with the pointer parked at the
|
||||
/// start of the payload.
|
||||
#[cfg(test)]
|
||||
pub fn framed(build: impl FnOnce(&mut Message)) -> Message {
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
build(&mut message);
|
||||
message.set_pointer(8);
|
||||
message
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_buffer() {
|
||||
let message = Message::new_with_data(
|
||||
@@ -462,8 +392,6 @@ fn get_message_code_on_short_message_does_not_panic() {
|
||||
let msg = Message::new_with_data(vec![0, 0, 0, 0]);
|
||||
assert_eq!(msg.get_message_code(), 0);
|
||||
assert_eq!(msg.get_message_code_u32(), 0);
|
||||
let empty = Message::new_with_data(vec![]);
|
||||
assert_eq!(empty.get_message_code_send(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -25,14 +25,14 @@ impl MessageHandler<PeerMessage> for PlaceInQueueRequest {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn the_asked_about_filename_is_forwarded() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("@@share\\track.mp3");
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("@@share\\track.mp3");
|
||||
});
|
||||
|
||||
PlaceInQueueRequest.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
|
||||
@@ -110,10 +110,9 @@ fn hostile_dir_count_does_not_hang() {
|
||||
// parse to empty promptly rather than looping into an OOM.
|
||||
let compressed =
|
||||
crate::utils::zlib::compress_stored(&u32::MAX.to_le_bytes());
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_raw_bytes(compressed);
|
||||
message.set_pointer(8);
|
||||
let mut message = crate::message::framed(|m| {
|
||||
m.write_raw_bytes(compressed);
|
||||
});
|
||||
assert!(parse_shared_file_list(&mut message).is_empty());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::info;
|
||||
use crate::{
|
||||
message::{Message, MessageHandler},
|
||||
peer::PeerMessage,
|
||||
types::UploadFailed,
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
@@ -12,7 +11,7 @@ impl MessageHandler<PeerMessage> for UploadFailedHandler {
|
||||
46
|
||||
}
|
||||
fn handle(&self, message: &mut Message, _sender: Sender<PeerMessage>) {
|
||||
let upload_failed = UploadFailed::new_from_message(message);
|
||||
info!("Upload failed for ${}", upload_failed.filename);
|
||||
let filename = message.read_string();
|
||||
info!("Upload failed for {}", filename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,16 +31,16 @@ impl MessageHandler<ServerMessage> for ExcludedSearchPhrasesHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn hostile_item_count_does_not_hang() {
|
||||
// item_count=u32::MAX with no phrases: the guard must make this return
|
||||
// promptly instead of looping ~4 billion times.
|
||||
let (tx, _rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_int32(u32::MAX);
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(u32::MAX);
|
||||
});
|
||||
ExcludedSearchPhrasesHandler.handle(&mut message, tx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,17 +33,16 @@ impl MessageHandler<ServerMessage> for JoinRoomHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn hostile_user_count_does_not_hang() {
|
||||
// room="" then user_count=u32::MAX with no usernames: must return
|
||||
// promptly instead of looping ~4 billion times.
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("");
|
||||
message.write_int32(u32::MAX);
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("").write_int32(u32::MAX);
|
||||
});
|
||||
|
||||
JoinRoomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
@@ -57,13 +56,12 @@ mod tests {
|
||||
#[test]
|
||||
fn forwards_room_and_member_list() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("nicotine");
|
||||
message.write_int32(2);
|
||||
message.write_string("alice");
|
||||
message.write_string("bob");
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("nicotine")
|
||||
.write_int32(2)
|
||||
.write_string("alice")
|
||||
.write_string("bob");
|
||||
});
|
||||
|
||||
JoinRoomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
|
||||
@@ -20,14 +20,14 @@ impl MessageHandler<ServerMessage> for LeaveRoomHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn forwards_left_room() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("jazz");
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("jazz");
|
||||
});
|
||||
|
||||
LeaveRoomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
|
||||
@@ -137,14 +137,6 @@ impl MessageFactory {
|
||||
.write_int32(port.into())
|
||||
.clone()
|
||||
}
|
||||
#[must_use]
|
||||
pub fn build_watch_user(token: u32) -> Message {
|
||||
Message::new()
|
||||
.write_raw_bytes([5, 0, 0, 0, 0].to_vec())
|
||||
.write_int32(token)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Ask the server (code 7) for a user's online status.
|
||||
#[must_use]
|
||||
pub fn build_get_user_status(username: &str) -> Message {
|
||||
@@ -280,15 +272,6 @@ fn test_build_get_user_stats() {
|
||||
assert_eq!(expect, message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_watch_user() {
|
||||
let token: u32 = 223;
|
||||
let message = MessageFactory::build_watch_user(token);
|
||||
let expect: Vec<u8> = [5, 0, 0, 0, 0, 223, 0, 0, 0].to_vec();
|
||||
|
||||
assert_eq!(expect, message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_login_message() {
|
||||
let message = MessageFactory::build_login_message(
|
||||
|
||||
@@ -27,7 +27,6 @@ impl MessageHandler<ServerMessage> for MessageUser {
|
||||
);
|
||||
|
||||
info!("[MessageUser] User message received:{:?}", user_message);
|
||||
user_message.print();
|
||||
|
||||
// Acknowledge freshly delivered messages so the server does not keep
|
||||
// re-sending them on every reconnect.
|
||||
|
||||
@@ -50,19 +50,12 @@ impl MessageHandler<ServerMessage> for CheckPrivilegesHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn payload(build: impl FnOnce(&mut Message)) -> Message {
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
build(&mut message);
|
||||
message.set_pointer(8);
|
||||
message
|
||||
}
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn the_user_list_is_kept_not_just_counted() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = payload(|m| {
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(2)
|
||||
.write_string("donor_a")
|
||||
.write_string("donor_b");
|
||||
@@ -80,7 +73,7 @@ mod tests {
|
||||
#[test]
|
||||
fn an_empty_list_is_still_an_answer() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = payload(|m| {
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(0);
|
||||
});
|
||||
|
||||
@@ -98,7 +91,7 @@ mod tests {
|
||||
// count=u32::MAX with no names: the guard must return promptly rather
|
||||
// than loop four billion times.
|
||||
let (tx, _rx) = std::sync::mpsc::channel();
|
||||
let mut message = payload(|m| {
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(u32::MAX);
|
||||
});
|
||||
PrivilegedUsersHandler.handle(&mut message, tx);
|
||||
@@ -107,7 +100,7 @@ mod tests {
|
||||
#[test]
|
||||
fn our_own_remaining_privilege_time_is_reported() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = payload(|m| {
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(86_400);
|
||||
});
|
||||
|
||||
|
||||
@@ -53,16 +53,7 @@ pub fn parse_room_list(message: &mut Message) -> Vec<RoomInfo> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build a received message (8-byte header + payload) and position it the
|
||||
/// way the dispatcher would.
|
||||
fn framed(build: impl FnOnce(&mut Message)) -> Message {
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
build(&mut message);
|
||||
message.set_pointer(8);
|
||||
message
|
||||
}
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn parses_names_zipped_with_counts() {
|
||||
|
||||
@@ -26,16 +26,16 @@ impl MessageHandler<ServerMessage> for SayChatroomHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn forwards_room_message() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("jazz");
|
||||
message.write_string("alice");
|
||||
message.write_string("hello everyone");
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("jazz")
|
||||
.write_string("alice")
|
||||
.write_string("hello everyone");
|
||||
});
|
||||
|
||||
SayChatroomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
|
||||
@@ -56,21 +56,12 @@ impl MessageHandler<ServerMessage> for GetUserStatsHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build a payload the dispatcher would hand a handler: 8 bytes of frame
|
||||
/// header, then the fields, with the pointer parked at the payload.
|
||||
fn payload(build: impl FnOnce(&mut Message)) -> Message {
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
build(&mut message);
|
||||
message.set_pointer(8);
|
||||
message
|
||||
}
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn user_status_is_parsed_with_its_privileged_flag() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = payload(|m| {
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("alice").write_int32(2).write_int8(1);
|
||||
});
|
||||
|
||||
@@ -93,7 +84,7 @@ mod tests {
|
||||
fn user_stats_skip_the_two_obsolete_fields() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
// username, speed, obsolete, obsolete, files, folders
|
||||
let mut message = payload(|m| {
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("bob")
|
||||
.write_int32(1024)
|
||||
.write_int32(0)
|
||||
|
||||
@@ -23,15 +23,14 @@ impl MessageHandler<ServerMessage> for UserJoinedRoomHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn forwards_user_joined() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("jazz");
|
||||
message.write_string("carol");
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("jazz").write_string("carol");
|
||||
});
|
||||
|
||||
UserJoinedRoomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
|
||||
@@ -21,15 +21,14 @@ impl MessageHandler<ServerMessage> for UserLeftRoomHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn forwards_user_left() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("jazz");
|
||||
message.write_string("carol");
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_string("jazz").write_string("carol");
|
||||
});
|
||||
|
||||
UserLeftRoomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
|
||||
@@ -26,14 +26,14 @@ impl MessageHandler<ServerMessage> for WishListIntervalHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::framed;
|
||||
|
||||
#[test]
|
||||
fn the_announced_interval_is_reported_not_discarded() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_int32(720);
|
||||
message.set_pointer(8);
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(720);
|
||||
});
|
||||
|
||||
WishListIntervalHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
|
||||
@@ -66,64 +66,42 @@ impl std::fmt::Display for DownloadError {
|
||||
|
||||
impl std::error::Error for DownloadError {}
|
||||
|
||||
impl From<io::Error> for DownloadError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::StreamReadError(error)
|
||||
}
|
||||
fn extract_filename_from_path(full_path: &str) -> &str {
|
||||
full_path
|
||||
.split(['/', '\\'])
|
||||
.next_back()
|
||||
.unwrap_or(full_path)
|
||||
}
|
||||
|
||||
struct FileManager;
|
||||
/// Where a download's bytes end up: its configured directory (falling back
|
||||
/// to the parent if it names a file) joined with the remote basename.
|
||||
fn resolve_download_path(download: &Download) -> Result<String, DownloadError> {
|
||||
let mut expanded_path = expand_tilde(&download.download_directory);
|
||||
|
||||
impl FileManager {
|
||||
fn extract_filename_from_path(full_path: &str) -> &str {
|
||||
full_path
|
||||
.split(['/', '\\'])
|
||||
.next_back()
|
||||
.unwrap_or(full_path)
|
||||
}
|
||||
|
||||
fn create_download_path_from_filename(
|
||||
output_directory: PathBuf,
|
||||
filename: &str,
|
||||
) -> PathBuf {
|
||||
let filename_only = Self::extract_filename_from_path(filename);
|
||||
output_directory.join(filename_only)
|
||||
}
|
||||
|
||||
/// Where a download's bytes end up: its configured directory (falling back
|
||||
/// to the parent if it names a file) joined with the remote basename.
|
||||
fn resolve_download_path(
|
||||
download: &Download,
|
||||
) -> Result<String, DownloadError> {
|
||||
let mut expanded_path = expand_tilde(&download.download_directory);
|
||||
|
||||
if !expanded_path.is_dir() {
|
||||
expanded_path = expanded_path
|
||||
.parent()
|
||||
.ok_or_else(|| {
|
||||
DownloadError::PathResolutionError(format!(
|
||||
"Cannot resolve parent directory for: {}",
|
||||
expanded_path.display()
|
||||
))
|
||||
})?
|
||||
.to_path_buf();
|
||||
}
|
||||
|
||||
let final_path = Self::create_download_path_from_filename(
|
||||
expanded_path,
|
||||
&download.filename,
|
||||
);
|
||||
|
||||
final_path
|
||||
.to_str()
|
||||
if !expanded_path.is_dir() {
|
||||
expanded_path = expanded_path
|
||||
.parent()
|
||||
.ok_or_else(|| {
|
||||
DownloadError::PathResolutionError(format!(
|
||||
"Path contains invalid UTF-8: {}",
|
||||
final_path.display()
|
||||
"Cannot resolve parent directory for: {}",
|
||||
expanded_path.display()
|
||||
))
|
||||
})
|
||||
.map(String::from)
|
||||
})?
|
||||
.to_path_buf();
|
||||
}
|
||||
|
||||
let final_path =
|
||||
expanded_path.join(extract_filename_from_path(&download.filename));
|
||||
|
||||
final_path
|
||||
.to_str()
|
||||
.ok_or_else(|| {
|
||||
DownloadError::PathResolutionError(format!(
|
||||
"Path contains invalid UTF-8: {}",
|
||||
final_path.display()
|
||||
))
|
||||
})
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
/// The `.part` file a transfer streams into, so an interrupted transfer leaves
|
||||
@@ -137,7 +115,7 @@ struct PartFile {
|
||||
|
||||
impl PartFile {
|
||||
fn open(download: &Download) -> Result<Self, DownloadError> {
|
||||
let final_path = FileManager::resolve_download_path(download)?;
|
||||
let final_path = resolve_download_path(download)?;
|
||||
let path = PathBuf::from(format!("{final_path}{PART_SUFFIX}"));
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
@@ -521,7 +499,9 @@ impl DownloadPeer {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DownloadError, DownloadPeer, FileManager, PartFile};
|
||||
use super::{
|
||||
DownloadError, DownloadPeer, PartFile, extract_filename_from_path,
|
||||
};
|
||||
use crate::types::{Download, DownloadMetadata, DownloadStatus};
|
||||
use std::sync::mpsc;
|
||||
|
||||
@@ -636,23 +616,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_extract_filename_from_path() {
|
||||
assert_eq!(extract_filename_from_path("/path/to/file.mp3"), "file.mp3");
|
||||
assert_eq!(
|
||||
FileManager::extract_filename_from_path("/path/to/file.mp3"),
|
||||
extract_filename_from_path("C:\\path\\to\\file.mp3"),
|
||||
"file.mp3"
|
||||
);
|
||||
assert_eq!(
|
||||
FileManager::extract_filename_from_path("C:\\path\\to\\file.mp3"),
|
||||
"file.mp3"
|
||||
);
|
||||
assert_eq!(
|
||||
FileManager::extract_filename_from_path(
|
||||
extract_filename_from_path(
|
||||
"@@bhfrv\\Soulseek Downloads\\complete\\Beatport Top Deep House (2021)\\michel test file.mp3"
|
||||
),
|
||||
"michel test file.mp3"
|
||||
);
|
||||
assert_eq!(
|
||||
FileManager::extract_filename_from_path("file.mp3"),
|
||||
"file.mp3"
|
||||
);
|
||||
assert_eq!(extract_filename_from_path("file.mp3"), "file.mp3");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,16 +77,6 @@ fn parse_peer_init_message(mut message: Message) -> Option<PeerInitData> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_token_from_buffer(buffer: &[u8], username: &str) -> Option<u32> {
|
||||
let token_bytes = buffer.get(0..4)?;
|
||||
let token = u32::from_le_bytes(
|
||||
token_bytes
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("[listener:{username}] slice with incorrect length, can't extract transfer_token")),
|
||||
);
|
||||
Some(token)
|
||||
}
|
||||
|
||||
fn extract_download_from_buffer(
|
||||
reader: &mut MessageReader,
|
||||
client_context: &Arc<RwLock<ClientContext>>,
|
||||
@@ -98,7 +88,7 @@ fn extract_download_from_buffer(
|
||||
return None;
|
||||
}
|
||||
let buffer = reader.get_buffer();
|
||||
let token = parse_token_from_buffer(&buffer, username)?;
|
||||
let token = u32::from_le_bytes(buffer.get(0..4)?.try_into().ok()?);
|
||||
trace!(
|
||||
"[listener:{}] got transfer_token: {} from data chunk",
|
||||
username, token
|
||||
@@ -129,8 +119,6 @@ fn handle_peer_connection(
|
||||
stream: TcpStream,
|
||||
reader: MessageReader,
|
||||
context: &ConnectionContext,
|
||||
_peer_ip: &str,
|
||||
_peer_port: u16,
|
||||
) {
|
||||
// The peer actor multiplexes socket reads with its mailbox on a single
|
||||
// thread: `tick()` reads the socket, but outgoing messages (e.g. a queued
|
||||
@@ -283,7 +271,7 @@ fn handle_pierce_firewall(
|
||||
0,
|
||||
0,
|
||||
);
|
||||
handle_peer_connection(peer, stream, reader, context, peer_ip, peer_port);
|
||||
handle_peer_connection(peer, stream, reader, context);
|
||||
|
||||
// Inbound peers don't self-announce, so nudge the client to flush any
|
||||
// downloads queued for this now-connected peer.
|
||||
@@ -292,7 +280,10 @@ fn handle_pierce_firewall(
|
||||
.send(ClientOperation::PeerConnected(username));
|
||||
}
|
||||
|
||||
fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
|
||||
fn handle_incoming_connection(
|
||||
mut stream: TcpStream,
|
||||
context: ConnectionContext,
|
||||
) {
|
||||
let Ok(peer_addr) = stream.peer_addr() else {
|
||||
error!("[listener] failed to get peer address");
|
||||
return;
|
||||
@@ -300,7 +291,6 @@ fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
|
||||
|
||||
let peer_ip = peer_addr.ip().to_string();
|
||||
let peer_port = peer_addr.port();
|
||||
let mut stream = stream;
|
||||
let mut reader = MessageReader::new();
|
||||
|
||||
// A peer that dials and then goes away is routine, not an error.
|
||||
@@ -337,7 +327,6 @@ fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
|
||||
|
||||
let peer = Peer::new(
|
||||
format!("{}:direct", init_data.username),
|
||||
// init_data.username.clone(),
|
||||
init_data.connection_type.clone(),
|
||||
peer_ip.clone(),
|
||||
peer_port.into(),
|
||||
@@ -348,9 +337,9 @@ fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
|
||||
);
|
||||
|
||||
match init_data.connection_type {
|
||||
ConnectionType::P => handle_peer_connection(
|
||||
peer, stream, reader, &context, &peer_ip, peer_port,
|
||||
),
|
||||
ConnectionType::P => {
|
||||
handle_peer_connection(peer, stream, reader, &context);
|
||||
}
|
||||
|
||||
ConnectionType::F => handle_file_connection(
|
||||
peer,
|
||||
|
||||
@@ -10,33 +10,7 @@ pub use download_peer::DownloadPeer;
|
||||
|
||||
use crate::message::Message;
|
||||
use core::fmt;
|
||||
use std::{net::TcpStream, str::FromStr};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct NewPeer {
|
||||
pub username: String,
|
||||
pub connection_type: ConnectionType,
|
||||
pub token: u32,
|
||||
pub tcp_stream: TcpStream,
|
||||
}
|
||||
impl NewPeer {
|
||||
pub fn new_from_message(
|
||||
message: &mut Message,
|
||||
tcp_stream: TcpStream,
|
||||
) -> Option<Self> {
|
||||
let username = message.read_string();
|
||||
let connection_type = message.read_string().parse().ok()?;
|
||||
let token = message.read_int32();
|
||||
|
||||
Some(Self {
|
||||
username,
|
||||
connection_type,
|
||||
token,
|
||||
tcp_stream,
|
||||
})
|
||||
}
|
||||
}
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ConnectionType {
|
||||
@@ -81,7 +55,6 @@ impl fmt::Display for ConnectionType {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Peer {
|
||||
pub username: String,
|
||||
pub connection_type: ConnectionType,
|
||||
@@ -93,7 +66,7 @@ pub struct Peer {
|
||||
pub obfuscated_port: Option<u16>,
|
||||
}
|
||||
impl Peer {
|
||||
#[allow(clippy::too_many_arguments, dead_code)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
username: String,
|
||||
@@ -116,7 +89,6 @@ impl Peer {
|
||||
obfuscated_port: Some(obfuscated_port),
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn new_from_message(message: &mut Message) -> Option<Self> {
|
||||
let username = message.read_string();
|
||||
// The connection type is an untrusted string; an unknown value must not
|
||||
|
||||
@@ -44,15 +44,7 @@ impl Shares {
|
||||
// Fail fast if the root is unreadable; deeper failures are skipped.
|
||||
let _ = std::fs::read_dir(root)?;
|
||||
|
||||
let mut files = Vec::new();
|
||||
let mut folder_count = 0;
|
||||
scan_root(
|
||||
root,
|
||||
&root_display_name(root),
|
||||
&mut files,
|
||||
&mut folder_count,
|
||||
);
|
||||
Ok(Self::from_files(files, folder_count))
|
||||
Ok(Self::scan_many(&[root.to_path_buf()]))
|
||||
}
|
||||
|
||||
/// Scan several roots into one merged index. Unreadable roots are
|
||||
|
||||
@@ -43,16 +43,6 @@ impl Default for ClientVersion {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UploadFailed {
|
||||
pub filename: String,
|
||||
}
|
||||
impl UploadFailed {
|
||||
pub fn new_from_message(message: &mut Message) -> Self {
|
||||
let filename = message.read_string();
|
||||
|
||||
Self { filename }
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SearchResult {
|
||||
|
||||
@@ -87,12 +87,12 @@ fn has_log_file() -> bool {
|
||||
pub fn log(level: LogLevel, message: &str) {
|
||||
unsafe {
|
||||
if level <= LOG_LEVEL {
|
||||
let level_str = match level {
|
||||
LogLevel::Error => "\x1b[31mERROR\x1b[0m", // Red
|
||||
LogLevel::Warn => "\x1b[33mWARN\x1b[0m", // Yellow
|
||||
LogLevel::Info => "\x1b[32mINFO\x1b[0m", // Green
|
||||
LogLevel::Debug => "\x1b[34mDEBUG\x1b[0m", // Blue
|
||||
LogLevel::Trace => "\x1b[35mTRACE\x1b[0m", // Magenta
|
||||
let (name, colour) = match level {
|
||||
LogLevel::Error => ("ERROR", "\x1b[31m"), // Red
|
||||
LogLevel::Warn => ("WARN", "\x1b[33m"), // Yellow
|
||||
LogLevel::Info => ("INFO", "\x1b[32m"), // Green
|
||||
LogLevel::Debug => ("DEBUG", "\x1b[34m"), // Blue
|
||||
LogLevel::Trace => ("TRACE", "\x1b[35m"), // Magenta
|
||||
};
|
||||
|
||||
let now = std::time::SystemTime::now();
|
||||
@@ -149,20 +149,8 @@ pub fn log(level: LogLevel, message: &str) {
|
||||
let minutes = (seconds_in_day % 3600) / 60;
|
||||
let seconds = seconds_in_day % 60;
|
||||
|
||||
let level_str_plain = match level {
|
||||
LogLevel::Error => "ERROR",
|
||||
LogLevel::Warn => "WARN",
|
||||
LogLevel::Info => "INFO",
|
||||
LogLevel::Debug => "DEBUG",
|
||||
LogLevel::Trace => "TRACE",
|
||||
};
|
||||
|
||||
let formatted_message = format!(
|
||||
"[{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{subsec_millis:03}] [{level_str}] {message}"
|
||||
);
|
||||
|
||||
let formatted_message_plain = format!(
|
||||
"[{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{subsec_millis:03}] [{level_str_plain}] {message}"
|
||||
let timestamp = format!(
|
||||
"{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{subsec_millis:03}"
|
||||
);
|
||||
|
||||
match choose_sink(BUFFERING.load(Ordering::Relaxed), has_log_file())
|
||||
@@ -171,17 +159,22 @@ pub fn log(level: LogLevel, message: &str) {
|
||||
if let Ok(mut log_file) = LOG_FILE.lock()
|
||||
&& let Some(file) = log_file.as_mut()
|
||||
{
|
||||
let _ = writeln!(file, "{formatted_message_plain}");
|
||||
let _ =
|
||||
writeln!(file, "[{timestamp}] [{name}] {message}");
|
||||
let _ = file.flush();
|
||||
}
|
||||
}
|
||||
LogSink::Buffer => {
|
||||
if let Ok(mut buffer) = BUFFER.lock() {
|
||||
buffer.push(formatted_message);
|
||||
buffer.push(format!(
|
||||
"[{timestamp}] [{colour}{name}\x1b[0m] {message}"
|
||||
));
|
||||
}
|
||||
}
|
||||
LogSink::Stderr => {
|
||||
eprintln!("{formatted_message}");
|
||||
eprintln!(
|
||||
"[{timestamp}] [{colour}{name}\x1b[0m] {message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,58 +189,19 @@ pub fn disable_buffering() {
|
||||
BUFFERING.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Only lines that were buffered land here, and a line is buffered only when
|
||||
/// no log file is configured, so the buffer is always stderr-bound.
|
||||
pub fn flush_buffered_logs() {
|
||||
disable_buffering();
|
||||
|
||||
if let Ok(mut buffer) = BUFFER.lock() {
|
||||
// Write to file if configured, otherwise to stderr
|
||||
if let Ok(mut log_file) = LOG_FILE.lock() {
|
||||
if let Some(file) = log_file.as_mut() {
|
||||
for message in buffer.iter() {
|
||||
// Strip ANSI codes for file output
|
||||
let plain_message = strip_ansi_codes(message);
|
||||
let _ = writeln!(file, "{plain_message}");
|
||||
}
|
||||
let _ = file.flush();
|
||||
} else {
|
||||
for message in buffer.iter() {
|
||||
eprintln!("{message}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If lock fails, fall back to stderr
|
||||
for message in buffer.iter() {
|
||||
eprintln!("{message}");
|
||||
}
|
||||
for message in buffer.iter() {
|
||||
eprintln!("{message}");
|
||||
}
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_ansi_codes(s: &str) -> String {
|
||||
let mut result = String::with_capacity(s.len());
|
||||
let mut chars = s.chars().peekable();
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\x1b' {
|
||||
// Skip ANSI escape sequence
|
||||
if chars.peek() == Some(&'[') {
|
||||
chars.next(); // consume '['
|
||||
while let Some(&ch) = chars.peek() {
|
||||
chars.next();
|
||||
if ch.is_ascii_alphabetic() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => {
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
//! No external crates are used — the library forbids dependencies, so the
|
||||
//! harness sticks to `std` and the library's own public API.
|
||||
|
||||
#![allow(clippy::doc_markdown)]
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
@@ -64,10 +62,11 @@ impl TestServer {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
||||
if let Ok(addr) = std::env::var("SOULSEEK_TEST_SERVER") {
|
||||
let (host, port) = split_host_port(&addr)?;
|
||||
wait_until_listening(&host, port, Duration::from_secs(2))?;
|
||||
let (host, port) = addr.rsplit_once(':')?;
|
||||
let port = port.parse().ok()?;
|
||||
wait_until_listening(host, port, Duration::from_secs(2))?;
|
||||
return Some(Self {
|
||||
host,
|
||||
host: host.to_string(),
|
||||
port,
|
||||
child: None,
|
||||
db: None,
|
||||
@@ -152,11 +151,6 @@ impl Drop for TestServer {
|
||||
}
|
||||
}
|
||||
|
||||
fn split_host_port(addr: &str) -> Option<(String, u16)> {
|
||||
let (host, port) = addr.rsplit_once(':')?;
|
||||
Some((host.to_string(), port.parse().ok()?))
|
||||
}
|
||||
|
||||
fn soulfind_binary() -> Option<PathBuf> {
|
||||
if let Ok(p) = std::env::var("SOULFIND_BIN") {
|
||||
let p = PathBuf::from(p);
|
||||
@@ -465,22 +459,6 @@ fn registered_username_can_relogin_with_same_password() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_clients_can_be_logged_in_together() {
|
||||
let server = server_or_skip!();
|
||||
|
||||
// The server must handle several independent sessions at once — this is
|
||||
// the precondition for any peer-to-peer feature routed through it.
|
||||
let mut alice = Client::with_settings(server.settings("e2e_alice", "pw_a"));
|
||||
let mut bob = Client::with_settings(server.settings("e2e_bob", "pw_b"));
|
||||
|
||||
alice.connect().expect("alice connect");
|
||||
bob.connect().expect("bob connect");
|
||||
|
||||
assert!(alice.login().expect("alice login"));
|
||||
assert!(bob.login().expect("bob login"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Peer-to-peer download coverage.
|
||||
//
|
||||
@@ -2149,7 +2127,6 @@ fn a_peer_asking_where_it_sits_is_answered_with_its_place() {
|
||||
);
|
||||
|
||||
// The asker keeps its own connection so it can read the answer back.
|
||||
// The asker keeps its connection so it can read the answer back.
|
||||
let asker_file = format!("{folder}\\asker.mp3");
|
||||
let mut asker =
|
||||
queue_as(&server_addr, &listen_addr, "e2e_q_asker", &asker_file)
|
||||
|
||||
Reference in New Issue
Block a user