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:
Michel de Graaf
2026-07-28 12:52:18 +02:00
parent cd56fb3b39
commit f1b667f7f9
62 changed files with 1367 additions and 2319 deletions
+27 -37
View File
@@ -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() {
+20 -82
View File
@@ -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);
+1 -37
View File
@@ -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);
}
}
+3 -27
View File
@@ -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
+58 -63
View File
@@ -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"));
+2 -2
View File
@@ -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);
+2 -3
View File
@@ -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
);
}
}
+12 -84
View File
@@ -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);
}
}
+10 -12
View File
@@ -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() {
+37 -63
View File
@@ -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");
}
}
+9 -20
View File
@@ -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,
+2 -30
View File
@@ -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
+1 -9
View File
@@ -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
-10
View File
@@ -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 {
+20 -66
View File
@@ -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)*) => {
+4 -27
View File
@@ -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)