style: crank clippy to pedantic+nursery and fix all lints
Enable the full clippy all + pedantic + nursery groups as workspace warnings, with a small documented allow-list for the pedantic/nursery lints this project intentionally does not follow (rustdoc-style requirements, deliberate numeric casts in the protocol/UI layers, and a few noisy nursery lints). Also enable the rust unused_lifetimes and unsafe_op_in_unsafe_fn lints. Fixed every resulting warning (527 -> 0) mechanically per clippy's suggestions: inline format args, const fns, item ordering, let-else, redundant clones, map_or, cast_lossless, Eq derives, associated-fn conversions for methods that never used self, and removing unnecessary Result wrappers whose call sites could never propagate an error. No runtime behavior changed; cargo fmt applied. cargo clippy --workspace --all-targets: 0 warnings. All tests pass.
This commit is contained in:
+39
@@ -8,3 +8,42 @@ authors = ["Michel de Graaf"]
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/michel/soulseek-rs"
|
repository = "https://github.com/michel/soulseek-rs"
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unused_lifetimes = "warn"
|
||||||
|
unsafe_op_in_unsafe_fn = "warn"
|
||||||
|
|
||||||
|
# Lints are cranked to the maximum practical level: the full clippy `all`,
|
||||||
|
# `pedantic`, and `nursery` groups are enabled as warnings. The allow-list below
|
||||||
|
# opts out of the pedantic/nursery lints that this project intentionally does
|
||||||
|
# not follow (documentation-style requirements, deliberate numeric casts in the
|
||||||
|
# protocol/UI layers, and a few noisy nursery lints), so that `cargo clippy`
|
||||||
|
# stays clean without churning code for no real benefit.
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
all = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
# Documentation-style pedantic lints — not tracked in rustdoc for this project.
|
||||||
|
missing_errors_doc = "allow"
|
||||||
|
missing_panics_doc = "allow"
|
||||||
|
doc_markdown = "allow"
|
||||||
|
# Numeric casts in the protocol framing and TUI layout are intentional and
|
||||||
|
# reviewed (the one real width-truncation bug was fixed separately).
|
||||||
|
cast_possible_truncation = "allow"
|
||||||
|
cast_precision_loss = "allow"
|
||||||
|
cast_sign_loss = "allow"
|
||||||
|
cast_possible_wrap = "allow"
|
||||||
|
# Message/handler APIs take ownership by design.
|
||||||
|
needless_pass_by_value = "allow"
|
||||||
|
# Long event loops and protocol parsers are clearer kept whole.
|
||||||
|
too_many_lines = "allow"
|
||||||
|
# Protocol test fixtures are raw byte/number arrays.
|
||||||
|
unreadable_literal = "allow"
|
||||||
|
# Nursery lint that fights RAII lock guards with frequent false positives.
|
||||||
|
significant_drop_tightening = "allow"
|
||||||
|
# Subjective nursery lint: the suggested map_or/map_or_else rewrites read worse
|
||||||
|
# than the explicit if let/match they replace.
|
||||||
|
option_if_let_else = "allow"
|
||||||
|
# The MD5 round functions use the conventional a/b/c/d register names from the
|
||||||
|
# RFC 1321 reference implementation.
|
||||||
|
many_single_char_names = "allow"
|
||||||
|
|||||||
@@ -13,4 +13,7 @@ categories = ["network-programming"]
|
|||||||
name = "soulseek_rs"
|
name = "soulseek_rs"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ impl<M: Send> ActorHandle<M> {
|
|||||||
pub fn send(&self, msg: M) -> Result<(), String> {
|
pub fn send(&self, msg: M) -> Result<(), String> {
|
||||||
self.sender
|
self.sender
|
||||||
.send(ActorMessage::UserMessage(msg))
|
.send(ActorMessage::UserMessage(msg))
|
||||||
.map_err(|e| format!("Failed to send message: {}", e))
|
.map_err(|e| format!("Failed to send message: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request actor to stop gracefully
|
/// Request actor to stop gracefully
|
||||||
pub fn stop(&self) -> Result<(), String> {
|
pub fn stop(&self) -> Result<(), String> {
|
||||||
self.sender
|
self.sender
|
||||||
.send(ActorMessage::Stop)
|
.send(ActorMessage::Stop)
|
||||||
.map_err(|e| format!("Failed to send stop signal: {}", e))
|
.map_err(|e| format!("Failed to send stop signal: {e}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,16 +64,15 @@ pub struct ActorSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ActorSystem {
|
impl ActorSystem {
|
||||||
pub fn new(thread_pool: Arc<ThreadPool>) -> Self {
|
#[must_use]
|
||||||
ActorSystem { thread_pool }
|
pub const fn new(thread_pool: Arc<ThreadPool>) -> Self {
|
||||||
|
Self { thread_pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a new actor and return its handle
|
/// Spawn a new actor and return its handle
|
||||||
pub fn spawn<A: Actor>(&self, mut actor: A) -> ActorHandle<A::Message> {
|
pub fn spawn<A: Actor>(&self, mut actor: A) -> ActorHandle<A::Message> {
|
||||||
let (sender, receiver) = channel::<ActorMessage<A::Message>>();
|
let (sender, receiver) = channel::<ActorMessage<A::Message>>();
|
||||||
let handle = ActorHandle {
|
let handle = ActorHandle { sender };
|
||||||
sender: sender.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start the actor event loop on the thread pool
|
// Start the actor event loop on the thread pool
|
||||||
self.thread_pool.execute(move || {
|
self.thread_pool.execute(move || {
|
||||||
@@ -86,7 +85,7 @@ impl ActorSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a new actor with initialization callback and return its handle
|
/// Spawn a new actor with initialization callback and return its handle
|
||||||
/// The callback receives the actor handle before on_start is called
|
/// The callback receives the actor handle before `on_start` is called
|
||||||
pub fn spawn_with_handle<A: Actor, F>(
|
pub fn spawn_with_handle<A: Actor, F>(
|
||||||
&self,
|
&self,
|
||||||
mut actor: A,
|
mut actor: A,
|
||||||
@@ -96,9 +95,7 @@ impl ActorSystem {
|
|||||||
F: FnOnce(&mut A, ActorHandle<A::Message>) + Send + 'static,
|
F: FnOnce(&mut A, ActorHandle<A::Message>) + Send + 'static,
|
||||||
{
|
{
|
||||||
let (sender, receiver) = channel::<ActorMessage<A::Message>>();
|
let (sender, receiver) = channel::<ActorMessage<A::Message>>();
|
||||||
let handle = ActorHandle {
|
let handle = ActorHandle { sender };
|
||||||
sender: sender.clone(),
|
|
||||||
};
|
|
||||||
let handle_for_init = handle.clone();
|
let handle_for_init = handle.clone();
|
||||||
|
|
||||||
self.thread_pool.execute(move || {
|
self.thread_pool.execute(move || {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ pub struct PeerActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PeerActor {
|
impl PeerActor {
|
||||||
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
peer: Peer,
|
peer: Peer,
|
||||||
stream: Option<TcpStream>,
|
stream: Option<TcpStream>,
|
||||||
@@ -135,9 +136,9 @@ impl PeerActor {
|
|||||||
msgs
|
msgs
|
||||||
});
|
});
|
||||||
|
|
||||||
messages
|
for msg in &messages {
|
||||||
.iter()
|
self.handle_message(msg.clone());
|
||||||
.for_each(|msg| self.handle_message(msg.clone()));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_message(&mut self, msg: PeerMessage) {
|
fn handle_message(&mut self, msg: PeerMessage) {
|
||||||
@@ -210,14 +211,7 @@ impl PeerActor {
|
|||||||
username, token, allowed
|
username, token, allowed
|
||||||
);
|
);
|
||||||
|
|
||||||
if !allowed {
|
if allowed {
|
||||||
if let Some(reason_text) = reason {
|
|
||||||
debug!(
|
|
||||||
"[peer:{}] Transfer rejected: {} - token {}, waiting for TransferRequest...",
|
|
||||||
username, reason_text, token
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!(
|
debug!(
|
||||||
"[peer:{}] Transfer allowed, ready to connect with token {:}",
|
"[peer:{}] Transfer allowed, ready to connect with token {:}",
|
||||||
username, token
|
username, token
|
||||||
@@ -237,6 +231,11 @@ impl PeerActor {
|
|||||||
username, e
|
username, e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if let Some(reason_text) = reason {
|
||||||
|
debug!(
|
||||||
|
"[peer:{}] Transfer rejected: {} - token {}, waiting for TransferRequest...",
|
||||||
|
username, reason_text, token
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PeerMessage::PlaceInQueueResponse { filename, place } => {
|
PeerMessage::PlaceInQueueResponse { filename, place } => {
|
||||||
@@ -267,7 +266,7 @@ impl PeerActor {
|
|||||||
match self.peer.write_safe() {
|
match self.peer.write_safe() {
|
||||||
Ok(mut p) => p.username = username,
|
Ok(mut p) => p.username = username,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("[peer_actor] SetUsername write: {}", e)
|
error!("[peer_actor] SetUsername write: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,9 +305,8 @@ impl PeerActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let stream = match self.stream.as_mut() {
|
let Some(stream) = self.stream.as_mut() else {
|
||||||
Some(s) => s,
|
return;
|
||||||
None => return,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.reader.read_from_socket(stream) {
|
match self.reader.read_from_socket(stream) {
|
||||||
@@ -352,7 +350,7 @@ impl PeerActor {
|
|||||||
message
|
message
|
||||||
.get_message_name(
|
.get_message_name(
|
||||||
MessageType::Peer,
|
MessageType::Peer,
|
||||||
message.get_message_code() as u32
|
u32::from(message.get_message_code())
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
);
|
);
|
||||||
@@ -381,12 +379,9 @@ impl PeerActor {
|
|||||||
|
|
||||||
fn send_message(&mut self, message: Message) {
|
fn send_message(&mut self, message: Message) {
|
||||||
let username = self.peer_username();
|
let username = self.peer_username();
|
||||||
let stream = match self.stream.as_mut() {
|
let Some(stream) = self.stream.as_mut() else {
|
||||||
Some(s) => s,
|
error!("Cannot send message: stream is None");
|
||||||
None => {
|
return;
|
||||||
error!("Cannot send message: stream is None");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
@@ -456,7 +451,7 @@ impl PeerActor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let socket_addr =
|
let socket_addr =
|
||||||
format!("{}:{}", host, port).parse::<std::net::SocketAddr>();
|
format!("{host}:{port}").parse::<std::net::SocketAddr>();
|
||||||
|
|
||||||
match socket_addr {
|
match socket_addr {
|
||||||
Ok(addr) => {
|
Ok(addr) => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub struct PeerRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PeerRegistry {
|
impl PeerRegistry {
|
||||||
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
actor_system: Arc<ActorSystem>,
|
actor_system: Arc<ActorSystem>,
|
||||||
client_channel: Sender<ClientOperation>,
|
client_channel: Sender<ClientOperation>,
|
||||||
@@ -48,7 +49,7 @@ impl PeerRegistry {
|
|||||||
let mut peers = self
|
let mut peers = self
|
||||||
.peers
|
.peers
|
||||||
.lock_safe()
|
.lock_safe()
|
||||||
.map_err(|e| format!("peer registry lock poisoned: {}", e))?;
|
.map_err(|e| format!("peer registry lock poisoned: {e}"))?;
|
||||||
// Stop any actor already registered under this username so it does not
|
// Stop any actor already registered under this username so it does not
|
||||||
// become an orphan pinning a pool worker forever (and so its later
|
// become an orphan pinning a pool worker forever (and so its later
|
||||||
// shutdown cannot remove this new connection from the registry).
|
// shutdown cannot remove this new connection from the registry).
|
||||||
@@ -64,6 +65,7 @@ impl PeerRegistry {
|
|||||||
Ok(handle)
|
Ok(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_peer(&self, username: &str) -> Option<ActorHandle<PeerMessage>> {
|
pub fn get_peer(&self, username: &str) -> Option<ActorHandle<PeerMessage>> {
|
||||||
match self.peers.lock_safe() {
|
match self.peers.lock_safe() {
|
||||||
Ok(peers) => peers.get(username).cloned(),
|
Ok(peers) => peers.get(username).cloned(),
|
||||||
@@ -74,6 +76,7 @@ impl PeerRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn remove_peer(
|
pub fn remove_peer(
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
@@ -94,6 +97,7 @@ impl PeerRegistry {
|
|||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_all_usernames(&self) -> Vec<String> {
|
pub fn get_all_usernames(&self) -> Vec<String> {
|
||||||
match self.peers.lock_safe() {
|
match self.peers.lock_safe() {
|
||||||
Ok(peers) => peers.keys().cloned().collect(),
|
Ok(peers) => peers.keys().cloned().collect(),
|
||||||
@@ -104,6 +108,7 @@ impl PeerRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn count(&self) -> usize {
|
pub fn count(&self) -> usize {
|
||||||
match self.peers.lock_safe() {
|
match self.peers.lock_safe() {
|
||||||
Ok(peers) => peers.len(),
|
Ok(peers) => peers.len(),
|
||||||
@@ -114,6 +119,7 @@ impl PeerRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn contains(&self, username: &str) -> bool {
|
pub fn contains(&self, username: &str) -> bool {
|
||||||
match self.peers.lock_safe() {
|
match self.peers.lock_safe() {
|
||||||
Ok(peers) => peers.contains_key(username),
|
Ok(peers) => peers.contains_key(username),
|
||||||
@@ -129,9 +135,9 @@ impl PeerRegistry {
|
|||||||
username: &str,
|
username: &str,
|
||||||
message: PeerMessage,
|
message: PeerMessage,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let handle = self.get_peer(username).ok_or_else(|| {
|
let handle = self
|
||||||
format!("Peer {} not found in registry", username)
|
.get_peer(username)
|
||||||
})?;
|
.ok_or_else(|| format!("Peer {username} not found in registry"))?;
|
||||||
|
|
||||||
handle.send(message)
|
handle.send(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,15 +34,18 @@ pub struct PeerAddress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PeerAddress {
|
impl PeerAddress {
|
||||||
pub fn new(host: String, port: u16) -> Self {
|
#[must_use]
|
||||||
|
pub const fn new(host: String, port: u16) -> Self {
|
||||||
Self { host, port }
|
Self { host, port }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_host(&self) -> &str {
|
pub fn get_host(&self) -> &str {
|
||||||
&self.host
|
&self.host
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_port(&self) -> u16 {
|
#[must_use]
|
||||||
|
pub const fn get_port(&self) -> u16 {
|
||||||
self.port
|
self.port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +62,7 @@ pub struct Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
@@ -72,7 +76,8 @@ pub struct UserMessage {
|
|||||||
new_message: bool,
|
new_message: bool,
|
||||||
}
|
}
|
||||||
impl UserMessage {
|
impl UserMessage {
|
||||||
pub fn new(
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
id: u32,
|
id: u32,
|
||||||
timestamp: u32,
|
timestamp: u32,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -143,6 +148,7 @@ pub struct ServerActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ServerActor {
|
impl ServerActor {
|
||||||
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
address: PeerAddress,
|
address: PeerAddress,
|
||||||
client_channel: Sender<ClientOperation>,
|
client_channel: Sender<ClientOperation>,
|
||||||
@@ -166,11 +172,13 @@ impl ServerActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_address(&self) -> &PeerAddress {
|
#[must_use]
|
||||||
|
pub const fn get_address(&self) -> &PeerAddress {
|
||||||
&self.address
|
&self.address
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_sender(&self) -> Option<&Sender<ServerMessage>> {
|
#[must_use]
|
||||||
|
pub const fn get_sender(&self) -> Option<&Sender<ServerMessage>> {
|
||||||
self.dispatcher_sender.as_ref()
|
self.dispatcher_sender.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +186,7 @@ impl ServerActor {
|
|||||||
let host = self.address.host.clone();
|
let host = self.address.host.clone();
|
||||||
let port = self.address.port;
|
let port = self.address.port;
|
||||||
|
|
||||||
let addr_str = format!("{}:{}", host, port);
|
let addr_str = format!("{host}:{port}");
|
||||||
|
|
||||||
let mut socket_addrs = match addr_str.to_socket_addrs() {
|
let mut socket_addrs = match addr_str.to_socket_addrs() {
|
||||||
Ok(addrs) => addrs,
|
Ok(addrs) => addrs,
|
||||||
@@ -197,7 +205,7 @@ impl ServerActor {
|
|||||||
|
|
||||||
let Some(addr) = socket_addr else {
|
let Some(addr) = socket_addr else {
|
||||||
let error_msg =
|
let error_msg =
|
||||||
format!("No socket addresses found for {}:{}", host, port);
|
format!("No socket addresses found for {host}:{port}");
|
||||||
error!("[server] {}", error_msg);
|
error!("[server] {}", error_msg);
|
||||||
self.disconnect_with_error(io::Error::new(
|
self.disconnect_with_error(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
@@ -280,9 +288,9 @@ impl ServerActor {
|
|||||||
msgs
|
msgs
|
||||||
});
|
});
|
||||||
|
|
||||||
messages
|
for msg in &messages {
|
||||||
.iter()
|
self.handle_message(msg.clone());
|
||||||
.for_each(|msg| self.handle_message(msg.clone()));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn login(
|
pub fn login(
|
||||||
@@ -305,7 +313,7 @@ impl ServerActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
logged_in = context.read_safe()?.logged_in
|
logged_in = context.read_safe()?.logged_in;
|
||||||
}
|
}
|
||||||
|
|
||||||
if logged_in.is_some() {
|
if logged_in.is_some() {
|
||||||
@@ -341,15 +349,12 @@ impl ServerActor {
|
|||||||
|
|
||||||
fn handle_message(&mut self, msg: ServerMessage) {
|
fn handle_message(&mut self, msg: ServerMessage) {
|
||||||
if !matches!(self.connection_state, ConnectionState::Connected) {
|
if !matches!(self.connection_state, ConnectionState::Connected) {
|
||||||
match &msg {
|
if matches!(&msg, ServerMessage::ProcessRead) {
|
||||||
ServerMessage::ProcessRead => {
|
// Always process read operations
|
||||||
// Always process read operations
|
} else {
|
||||||
}
|
// Queue all other messages when not connected
|
||||||
_ => {
|
self.queued_messages.push(msg);
|
||||||
// Queue all other messages when not connected
|
return;
|
||||||
self.queued_messages.push(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +362,7 @@ impl ServerActor {
|
|||||||
ServerMessage::ConnectToPeer(peer) => {
|
ServerMessage::ConnectToPeer(peer) => {
|
||||||
if let Some(op) = match peer.connection_type {
|
if let Some(op) = match peer.connection_type {
|
||||||
ConnectionType::P | ConnectionType::F => {
|
ConnectionType::P | ConnectionType::F => {
|
||||||
Some(ClientOperation::ConnectToPeer(peer.clone()))
|
Some(ClientOperation::ConnectToPeer(peer))
|
||||||
}
|
}
|
||||||
ConnectionType::D => None,
|
ConnectionType::D => None,
|
||||||
} && let Err(e) = self.client_channel.send(op)
|
} && let Err(e) = self.client_channel.send(op)
|
||||||
@@ -369,7 +374,7 @@ impl ServerActor {
|
|||||||
match self.context.write_safe() {
|
match self.context.write_safe() {
|
||||||
Ok(mut ctx) => ctx.logged_in = Some(message),
|
Ok(mut ctx) => ctx.logged_in = Some(message),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("[server] LoginStatus write: {}", e)
|
error!("[server] LoginStatus write: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,9 +474,8 @@ impl ServerActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let stream = match self.stream.as_mut() {
|
let Some(stream) = self.stream.as_mut() else {
|
||||||
Some(s) => s,
|
return;
|
||||||
None => return,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.reader.read_from_socket(stream) {
|
match self.reader.read_from_socket(stream) {
|
||||||
@@ -506,7 +510,7 @@ impl ServerActor {
|
|||||||
message
|
message
|
||||||
.get_message_name(
|
.get_message_name(
|
||||||
MessageType::Server,
|
MessageType::Server,
|
||||||
message.get_message_code() as u32
|
u32::from(message.get_message_code())
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
);
|
);
|
||||||
@@ -536,7 +540,7 @@ impl ServerActor {
|
|||||||
fn queue_message(&mut self, message: Message) {
|
fn queue_message(&mut self, message: Message) {
|
||||||
if let Some(sender) = &self.dispatcher_sender {
|
if let Some(sender) = &self.dispatcher_sender {
|
||||||
match sender.send(ServerMessage::SendMessage(message)) {
|
match sender.send(ServerMessage::SendMessage(message)) {
|
||||||
Ok(_) => {}
|
Ok(()) => {}
|
||||||
Err(e) => error!("Failed to send: {}", e),
|
Err(e) => error!("Failed to send: {}", e),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -546,12 +550,9 @@ impl ServerActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send_message(&mut self, message: Message) {
|
fn send_message(&mut self, message: Message) {
|
||||||
let stream = match self.stream.as_mut() {
|
let Some(stream) = self.stream.as_mut() else {
|
||||||
Some(s) => s,
|
error!("[server] Cannot send message: stream is None");
|
||||||
None => {
|
return;
|
||||||
error!("[server] Cannot send message: stream is None");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ impl ClientContext {
|
|||||||
pub fn remove_download(&mut self, token: u32) {
|
pub fn remove_download(&mut self, token: u32) {
|
||||||
self.downloads.remove(token);
|
self.downloads.remove(token);
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn get_download_by_token(&self, token: u32) -> Option<&Download> {
|
pub fn get_download_by_token(&self, token: u32) -> Option<&Download> {
|
||||||
self.downloads.get_by_token(token)
|
self.downloads.get_by_token(token)
|
||||||
}
|
}
|
||||||
@@ -130,10 +131,12 @@ impl ClientContext {
|
|||||||
) -> Option<&mut Download> {
|
) -> Option<&mut Download> {
|
||||||
self.downloads.get_by_file_mut(username, filename)
|
self.downloads.get_by_file_mut(username, filename)
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn get_download_tokens(&self) -> Vec<u32> {
|
pub fn get_download_tokens(&self) -> Vec<u32> {
|
||||||
self.downloads.tokens()
|
self.downloads.tokens()
|
||||||
}
|
}
|
||||||
pub fn get_downloads(&self) -> &Vec<Download> {
|
#[must_use]
|
||||||
|
pub const fn get_downloads(&self) -> &Vec<Download> {
|
||||||
self.downloads.list()
|
self.downloads.list()
|
||||||
}
|
}
|
||||||
pub fn update_download_with_status(
|
pub fn update_download_with_status(
|
||||||
@@ -165,7 +168,7 @@ fn test_client_context_downloads() {
|
|||||||
status: DownloadStatus::Queued,
|
status: DownloadStatus::Queued,
|
||||||
sender: mpsc::channel().0,
|
sender: mpsc::channel().0,
|
||||||
queue_position: None,
|
queue_position: None,
|
||||||
metadata: Default::default(),
|
metadata: DownloadMetadata::default(),
|
||||||
};
|
};
|
||||||
context.add_download(download);
|
context.add_download(download);
|
||||||
assert!(context.get_download_by_token(123).is_some());
|
assert!(context.get_download_by_token(123).is_some());
|
||||||
@@ -173,7 +176,7 @@ fn test_client_context_downloads() {
|
|||||||
assert_eq!(context.get_downloads().len(), 1);
|
assert_eq!(context.get_downloads().len(), 1);
|
||||||
if let Some(download) = context.get_download_by_token_mut(token) {
|
if let Some(download) = context.get_download_by_token_mut(token) {
|
||||||
assert_eq!(download.token, token);
|
assert_eq!(download.token, token);
|
||||||
download.token = new_token
|
download.token = new_token;
|
||||||
}
|
}
|
||||||
assert!(context.get_download_by_token(new_token).is_some());
|
assert!(context.get_download_by_token(new_token).is_some());
|
||||||
assert_eq!(context.get_download_tokens(), vec![new_token]);
|
assert_eq!(context.get_download_tokens(), vec![new_token]);
|
||||||
@@ -199,7 +202,7 @@ fn test_client_pause_and_resume_download() {
|
|||||||
},
|
},
|
||||||
sender: download_sender,
|
sender: download_sender,
|
||||||
queue_position: None,
|
queue_position: None,
|
||||||
metadata: Default::default(),
|
metadata: DownloadMetadata::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
client.context.write().unwrap().add_download(download);
|
client.context.write().unwrap().add_download(download);
|
||||||
@@ -255,7 +258,7 @@ fn test_client_removes_only_queued_downloads() {
|
|||||||
status: DownloadStatus::Queued,
|
status: DownloadStatus::Queued,
|
||||||
sender: mpsc::channel().0,
|
sender: mpsc::channel().0,
|
||||||
queue_position: None,
|
queue_position: None,
|
||||||
metadata: Default::default(),
|
metadata: DownloadMetadata::default(),
|
||||||
};
|
};
|
||||||
let active_download = Download {
|
let active_download = Download {
|
||||||
username: "peer".to_string(),
|
username: "peer".to_string(),
|
||||||
@@ -270,7 +273,7 @@ fn test_client_removes_only_queued_downloads() {
|
|||||||
},
|
},
|
||||||
sender: mpsc::channel().0,
|
sender: mpsc::channel().0,
|
||||||
queue_position: None,
|
queue_position: None,
|
||||||
metadata: Default::default(),
|
metadata: DownloadMetadata::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -289,12 +292,11 @@ fn test_client_removes_only_queued_downloads() {
|
|||||||
impl ClientContext {
|
impl ClientContext {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let max_threads = thread::available_parallelism()
|
let max_threads =
|
||||||
.map(|n| n.get())
|
thread::available_parallelism().map_or(8, std::num::NonZero::get);
|
||||||
.unwrap_or(8);
|
|
||||||
|
|
||||||
let thread_pool = Arc::new(ThreadPool::new(max_threads));
|
let thread_pool = Arc::new(ThreadPool::new(max_threads));
|
||||||
let actor_system = Arc::new(ActorSystem::new(thread_pool.clone()));
|
let actor_system = Arc::new(ActorSystem::new(thread_pool));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
peer_registry: None,
|
peer_registry: None,
|
||||||
@@ -324,6 +326,7 @@ impl Client {
|
|||||||
Self::with_settings(ClientSettings::new(username, password))
|
Self::with_settings(ClientSettings::new(username, password))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_settings(settings: ClientSettings) -> Self {
|
pub fn with_settings(settings: ClientSettings) -> Self {
|
||||||
logger::init();
|
logger::init();
|
||||||
Self {
|
Self {
|
||||||
@@ -483,6 +486,7 @@ impl Client {
|
|||||||
Ok(self.get_search_results(query))
|
Ok(self.get_search_results(query))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_search_results_count(&self, search_key: &str) -> usize {
|
pub fn get_search_results_count(&self, search_key: &str) -> usize {
|
||||||
self.context
|
self.context
|
||||||
.read_safe()
|
.read_safe()
|
||||||
@@ -493,6 +497,7 @@ impl Client {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_search_results(&self, search_key: &str) -> Vec<SearchResult> {
|
pub fn get_search_results(&self, search_key: &str) -> Vec<SearchResult> {
|
||||||
self.context
|
self.context
|
||||||
.read_safe()
|
.read_safe()
|
||||||
@@ -504,6 +509,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Non-blocking variant that returns None if the lock is unavailable
|
/// Non-blocking variant that returns None if the lock is unavailable
|
||||||
|
#[must_use]
|
||||||
pub fn try_get_search_results(
|
pub fn try_get_search_results(
|
||||||
&self,
|
&self,
|
||||||
search_key: &str,
|
search_key: &str,
|
||||||
@@ -513,6 +519,7 @@ impl Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_all_searches(&self) -> HashMap<String, Search> {
|
pub fn get_all_searches(&self) -> HashMap<String, Search> {
|
||||||
self.context
|
self.context
|
||||||
.read_safe()
|
.read_safe()
|
||||||
@@ -520,6 +527,7 @@ impl Client {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_all_downloads(&self) -> Vec<Download> {
|
pub fn get_all_downloads(&self) -> Vec<Download> {
|
||||||
self.context
|
self.context
|
||||||
.read_safe()
|
.read_safe()
|
||||||
@@ -527,6 +535,7 @@ impl Client {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn pause_download(&self, username: &str, filename: &str) -> bool {
|
pub fn pause_download(&self, username: &str, filename: &str) -> bool {
|
||||||
match self.context.write_safe() {
|
match self.context.write_safe() {
|
||||||
Ok(mut ctx) => ctx.downloads.pause_by_file(username, filename),
|
Ok(mut ctx) => ctx.downloads.pause_by_file(username, filename),
|
||||||
@@ -537,6 +546,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn resume_download(&self, username: &str, filename: &str) -> bool {
|
pub fn resume_download(&self, username: &str, filename: &str) -> bool {
|
||||||
match self.context.write_safe() {
|
match self.context.write_safe() {
|
||||||
Ok(mut ctx) => ctx.downloads.resume_by_file(username, filename),
|
Ok(mut ctx) => ctx.downloads.resume_by_file(username, filename),
|
||||||
@@ -547,6 +557,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn remove_queued_download(
|
pub fn remove_queued_download(
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
@@ -599,7 +610,7 @@ impl Client {
|
|||||||
|
|
||||||
let download = Download {
|
let download = Download {
|
||||||
username: username.clone(),
|
username: username.clone(),
|
||||||
filename: filename.clone(),
|
filename,
|
||||||
token,
|
token,
|
||||||
size,
|
size,
|
||||||
download_directory,
|
download_directory,
|
||||||
@@ -728,17 +739,18 @@ impl Client {
|
|||||||
// client ops loop if this read guard were still
|
// client ops loop if this read guard were still
|
||||||
// held on this thread.
|
// held on this thread.
|
||||||
{
|
{
|
||||||
let context =
|
let context = match client_context
|
||||||
match client_context.read_safe() {
|
.read_safe()
|
||||||
Ok(c) => c,
|
{
|
||||||
Err(e) => {
|
Ok(c) => c,
|
||||||
error!(
|
Err(e) => {
|
||||||
"[client] PeerDisconnected read: {}",
|
error!(
|
||||||
e
|
"[client] PeerDisconnected read: {}",
|
||||||
);
|
e
|
||||||
continue;
|
);
|
||||||
}
|
continue;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
if let Some(ref registry) =
|
if let Some(ref registry) =
|
||||||
context.peer_registry
|
context.peer_registry
|
||||||
&& let Some(handle) =
|
&& let Some(handle) =
|
||||||
@@ -864,17 +876,17 @@ impl Client {
|
|||||||
token
|
token
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
ClientOperation::NewPeer(new_peer) => {
|
ClientOperation::NewPeer(new_peer) => {
|
||||||
let peer_exists = match client_context
|
let peer_exists = match client_context
|
||||||
.read_safe()
|
.read_safe()
|
||||||
{
|
{
|
||||||
Ok(ctx) => ctx
|
Ok(ctx) => {
|
||||||
.peer_registry
|
ctx.peer_registry.as_ref().is_some_and(
|
||||||
.as_ref()
|
|r| r.contains(&new_peer.username),
|
||||||
.map(|r| r.contains(&new_peer.username))
|
)
|
||||||
.unwrap_or(false),
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("[client] NewPeer read: {}", e);
|
error!("[client] NewPeer read: {}", e);
|
||||||
continue;
|
continue;
|
||||||
@@ -965,8 +977,7 @@ impl Client {
|
|||||||
Ok(ctx) => ctx
|
Ok(ctx) => ctx
|
||||||
.peer_registry
|
.peer_registry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|r| r.contains(&username))
|
.is_some_and(|r| r.contains(&username)),
|
||||||
.unwrap_or(false),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"[client] GetPeerAddressResponse read: {}",
|
"[client] GetPeerAddressResponse read: {}",
|
||||||
@@ -1121,7 +1132,7 @@ impl Client {
|
|||||||
own_username: String,
|
own_username: String,
|
||||||
stream: Option<TcpStream>,
|
stream: Option<TcpStream>,
|
||||||
) {
|
) {
|
||||||
let client_context = client_context.clone();
|
let client_context = client_context;
|
||||||
|
|
||||||
let peer_clone = peer.clone();
|
let peer_clone = peer.clone();
|
||||||
trace!(
|
trace!(
|
||||||
@@ -1130,7 +1141,7 @@ impl Client {
|
|||||||
);
|
);
|
||||||
match peer.connection_type {
|
match peer.connection_type {
|
||||||
ConnectionType::P => {
|
ConnectionType::P => {
|
||||||
let username = peer.username.clone();
|
let username = peer.username;
|
||||||
|
|
||||||
let context = match client_context.read_safe() {
|
let context = match client_context.read_safe() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -1165,7 +1176,7 @@ impl Client {
|
|||||||
peer.port,
|
peer.port,
|
||||||
peer.token.unwrap(),
|
peer.token.unwrap(),
|
||||||
false,
|
false,
|
||||||
own_username.clone(),
|
own_username,
|
||||||
);
|
);
|
||||||
|
|
||||||
match download_peer.download_file(
|
match download_peer.download_file(
|
||||||
@@ -1196,7 +1207,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConnectionType::D => {
|
ConnectionType::D => {
|
||||||
error!("ConnectionType::D not implemented")
|
error!("ConnectionType::D not implemented");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1217,9 +1228,9 @@ impl Client {
|
|||||||
if let Some(server_sender) = &context.server_sender {
|
if let Some(server_sender) = &context.server_sender {
|
||||||
if let Some(token) = peer.token {
|
if let Some(token) = peer.token {
|
||||||
match server_sender.send(ServerMessage::PierceFirewall(token)) {
|
match server_sender.send(ServerMessage::PierceFirewall(token)) {
|
||||||
Ok(_) => (),
|
Ok(()) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to send PierceFirewall message: {}", e)
|
error!("Failed to send PierceFirewall message: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ pub struct MessageDispatcher<Op> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<Op> MessageDispatcher<Op> {
|
impl<Op> MessageDispatcher<Op> {
|
||||||
pub fn new(
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
owner_name: String,
|
owner_name: String,
|
||||||
sender: Sender<Op>,
|
sender: Sender<Op>,
|
||||||
handlers: Handlers<Op>,
|
handlers: Handlers<Op>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
MessageDispatcher {
|
Self {
|
||||||
owner_name,
|
owner_name,
|
||||||
sender,
|
sender,
|
||||||
handlers,
|
handlers,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub struct DownloadStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DownloadStore {
|
impl DownloadStore {
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
@@ -18,6 +19,7 @@ impl DownloadStore {
|
|||||||
self.downloads.retain(|d| d.token != token);
|
self.downloads.retain(|d| d.token != token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_by_token(&self, token: u32) -> Option<&Download> {
|
pub fn get_by_token(&self, token: u32) -> Option<&Download> {
|
||||||
self.downloads.iter().find(|d| d.token == token)
|
self.downloads.iter().find(|d| d.token == token)
|
||||||
}
|
}
|
||||||
@@ -36,11 +38,13 @@ impl DownloadStore {
|
|||||||
.find(|d| d.username == username && d.filename == filename)
|
.find(|d| d.username == username && d.filename == filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn tokens(&self) -> Vec<u32> {
|
pub fn tokens(&self) -> Vec<u32> {
|
||||||
self.downloads.iter().map(|d| d.token).collect()
|
self.downloads.iter().map(|d| d.token).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(&self) -> &Vec<Download> {
|
#[must_use]
|
||||||
|
pub const fn list(&self) -> &Vec<Download> {
|
||||||
&self.downloads
|
&self.downloads
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,9 +132,11 @@ impl DownloadStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the tokens of downloads matching `username` (and optionally a
|
/// Returns the tokens of downloads matching `username` (and optionally a
|
||||||
/// `filename`) after notifying their senders of `Failed`. Caller is
|
/// `filename`) after notifying their senders of `Failed`.
|
||||||
/// responsible for then calling `update_status` and `remove` for each token,
|
///
|
||||||
/// typically under a write lock.
|
/// Caller is responsible for then calling `update_status` and `remove` for
|
||||||
|
/// each token, typically under a write lock.
|
||||||
|
#[must_use]
|
||||||
pub fn collect_failed_tokens(
|
pub fn collect_failed_tokens(
|
||||||
store: &DownloadStore,
|
store: &DownloadStore,
|
||||||
username: &str,
|
username: &str,
|
||||||
@@ -152,19 +158,20 @@ pub fn collect_failed_tokens(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::types::DownloadMetadata;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
fn make_download(token: u32, status: DownloadStatus) -> Download {
|
fn make_download(token: u32, status: DownloadStatus) -> Download {
|
||||||
Download {
|
Download {
|
||||||
username: "peer".to_string(),
|
username: "peer".to_string(),
|
||||||
filename: format!("file-{}.mp3", token),
|
filename: format!("file-{token}.mp3"),
|
||||||
token,
|
token,
|
||||||
size: 100,
|
size: 100,
|
||||||
download_directory: "test".to_string(),
|
download_directory: "test".to_string(),
|
||||||
status,
|
status,
|
||||||
sender: mpsc::channel().0,
|
sender: mpsc::channel().0,
|
||||||
queue_position: None,
|
queue_position: None,
|
||||||
metadata: Default::default(),
|
metadata: DownloadMetadata::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,23 +25,23 @@ pub enum SoulseekRs {
|
|||||||
impl fmt::Display for SoulseekRs {
|
impl fmt::Display for SoulseekRs {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
SoulseekRs::NetworkError(err) => {
|
Self::NetworkError(err) => {
|
||||||
write!(f, "Network error: {}", err)
|
write!(f, "Network error: {err}")
|
||||||
}
|
}
|
||||||
SoulseekRs::AuthenticationFailed => {
|
Self::AuthenticationFailed => {
|
||||||
write!(f, "Authentication failed")
|
write!(f, "Authentication failed")
|
||||||
}
|
}
|
||||||
SoulseekRs::ParseError(msg) => write!(f, "Parse error: {}", msg),
|
Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
|
||||||
SoulseekRs::Timeout => write!(f, "Operation timed out"),
|
Self::Timeout => write!(f, "Operation timed out"),
|
||||||
SoulseekRs::ConnectionClosed => write!(f, "Connection closed"),
|
Self::ConnectionClosed => write!(f, "Connection closed"),
|
||||||
SoulseekRs::InvalidMessage(msg) => {
|
Self::InvalidMessage(msg) => {
|
||||||
write!(f, "Invalid message: {}", msg)
|
write!(f, "Invalid message: {msg}")
|
||||||
}
|
}
|
||||||
SoulseekRs::NotConnected => write!(f, "Not connected to server"),
|
Self::NotConnected => write!(f, "Not connected to server"),
|
||||||
SoulseekRs::CompressionError(msg) => {
|
Self::CompressionError(msg) => {
|
||||||
write!(f, "Compression error: {}", msg)
|
write!(f, "Compression error: {msg}")
|
||||||
}
|
}
|
||||||
SoulseekRs::LockPoisoned => {
|
Self::LockPoisoned => {
|
||||||
write!(f, "Lock poisoned by panicking thread")
|
write!(f, "Lock poisoned by panicking thread")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ impl fmt::Display for SoulseekRs {
|
|||||||
impl Error for SoulseekRs {
|
impl Error for SoulseekRs {
|
||||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
SoulseekRs::NetworkError(err) => Some(err),
|
Self::NetworkError(err) => Some(err),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,19 +59,19 @@ impl Error for SoulseekRs {
|
|||||||
|
|
||||||
impl From<std::io::Error> for SoulseekRs {
|
impl From<std::io::Error> for SoulseekRs {
|
||||||
fn from(err: std::io::Error) -> Self {
|
fn from(err: std::io::Error) -> Self {
|
||||||
SoulseekRs::NetworkError(err)
|
Self::NetworkError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::num::ParseIntError> for SoulseekRs {
|
impl From<std::num::ParseIntError> for SoulseekRs {
|
||||||
fn from(err: std::num::ParseIntError) -> Self {
|
fn from(err: std::num::ParseIntError) -> Self {
|
||||||
SoulseekRs::ParseError(format!("Integer parse error: {}", err))
|
Self::ParseError(format!("Integer parse error: {err}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for SoulseekRs {
|
impl From<String> for SoulseekRs {
|
||||||
fn from(err: String) -> Self {
|
fn from(err: String) -> Self {
|
||||||
SoulseekRs::CompressionError(err)
|
Self::CompressionError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ impl<Op> Default for Handlers<Op> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<Op> Handlers<Op> {
|
impl<Op> Handlers<Op> {
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
handlers: HashMap::new(),
|
handlers: HashMap::new(),
|
||||||
@@ -31,6 +32,7 @@ impl<Op> Handlers<Op> {
|
|||||||
self.handlers.insert(handler.get_code(), Box::new(handler));
|
self.handlers.insert(handler.get_code(), Box::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn get_handler(
|
pub fn get_handler(
|
||||||
&self,
|
&self,
|
||||||
code: u8,
|
code: u8,
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ impl Default for MessageReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageReader {
|
impl MessageReader {
|
||||||
pub fn new() -> Self {
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer: VecDeque::new(),
|
buffer: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[must_use]
|
||||||
pub fn new_with_buffer(buffer: Vec<u8>) -> Self {
|
pub fn new_with_buffer(buffer: Vec<u8>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer: buffer.into(),
|
buffer: buffer.into(),
|
||||||
@@ -47,6 +49,7 @@ impl MessageReader {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn buffer_len(&self) -> usize {
|
pub fn buffer_len(&self) -> usize {
|
||||||
self.buffer.len()
|
self.buffer.len()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub use message_reader::MessageReader;
|
|||||||
|
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum MessageType {
|
pub enum MessageType {
|
||||||
Server,
|
Server,
|
||||||
@@ -30,7 +30,7 @@ impl std::fmt::Display for Error {
|
|||||||
|
|
||||||
impl std::error::Error for Error {}
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
pointer: usize,
|
pointer: usize,
|
||||||
@@ -43,7 +43,8 @@ impl Default for Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
pub fn new() -> Self {
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
data: Vec::new(),
|
data: Vec::new(),
|
||||||
pointer: 0,
|
pointer: 0,
|
||||||
@@ -51,8 +52,8 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_hex(&self) {
|
pub fn print_hex(&self) {
|
||||||
let data = &self.data;
|
|
||||||
const BYTES_PER_LINE: usize = 16;
|
const BYTES_PER_LINE: usize = 16;
|
||||||
|
let data = &self.data;
|
||||||
|
|
||||||
let chunks = data.chunks(BYTES_PER_LINE);
|
let chunks = data.chunks(BYTES_PER_LINE);
|
||||||
for (i, chunk) in chunks.enumerate() {
|
for (i, chunk) in chunks.enumerate() {
|
||||||
@@ -95,6 +96,7 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[must_use]
|
||||||
pub fn get_message_code_u32(&self) -> u32 {
|
pub fn get_message_code_u32(&self) -> u32 {
|
||||||
if self.data.len() < 8 {
|
if self.data.len() < 8 {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -102,6 +104,7 @@ impl Message {
|
|||||||
u32::from_le_bytes(self.data[4..8].try_into().unwrap())
|
u32::from_le_bytes(self.data[4..8].try_into().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_message_code(&self) -> u8 {
|
pub fn get_message_code(&self) -> u8 {
|
||||||
if self.data.len() <= 4 {
|
if self.data.len() <= 4 {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -109,6 +112,7 @@ impl Message {
|
|||||||
self.data[4]
|
self.data[4]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_message_code_send(&self) -> u8 {
|
pub fn get_message_code_send(&self) -> u8 {
|
||||||
if self.data.is_empty() {
|
if self.data.is_empty() {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -116,34 +120,38 @@ impl Message {
|
|||||||
self.data[0]
|
self.data[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_data(data: Vec<u8>) -> Self {
|
#[must_use]
|
||||||
|
pub const fn new_with_data(data: Vec<u8>) -> Self {
|
||||||
Self { data, pointer: 0 }
|
Self { data, pointer: 0 }
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn reset_pointer(&mut self) {
|
pub const fn reset_pointer(&mut self) {
|
||||||
self.pointer = 0;
|
self.pointer = 0;
|
||||||
}
|
}
|
||||||
pub fn set_pointer(&mut self, pointer: usize) {
|
pub const fn set_pointer(&mut self, pointer: usize) {
|
||||||
self.pointer = pointer;
|
self.pointer = pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pointer(&mut self) -> usize {
|
pub const fn get_pointer(&mut self) -> usize {
|
||||||
self.pointer
|
self.pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_size(&mut self) -> usize {
|
pub const fn get_size(&mut self) -> usize {
|
||||||
self.data.len()
|
self.data.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_data(&self) -> Vec<u8> {
|
pub fn get_data(&self) -> Vec<u8> {
|
||||||
self.data.clone()
|
self.data.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn get_slice(&self, from: usize, to: usize) -> Vec<u8> {
|
pub fn get_slice(&self, from: usize, to: usize) -> Vec<u8> {
|
||||||
self.data[from..to].to_vec()
|
self.data[from..to].to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// gets buffer with the message length prepended
|
/// gets buffer with the message length prepended
|
||||||
|
#[must_use]
|
||||||
pub fn get_buffer(&self) -> Vec<u8> {
|
pub fn get_buffer(&self) -> Vec<u8> {
|
||||||
let mut b = vec![0u8; 4];
|
let mut b = vec![0u8; 4];
|
||||||
let length = self.data.len() as u32;
|
let length = self.data.len() as u32;
|
||||||
@@ -277,7 +285,7 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_bool(&mut self, value: bool) -> &mut Self {
|
pub fn write_bool(&mut self, value: bool) -> &mut Self {
|
||||||
self.data.push(if value { 1 } else { 0 });
|
self.data.push(u8::from(value));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,16 +341,13 @@ impl Message {
|
|||||||
104 => Ok("WishlistInterval"),
|
104 => Ok("WishlistInterval"),
|
||||||
160 => Ok("ExcludedSearchPhrases"),
|
160 => Ok("ExcludedSearchPhrases"),
|
||||||
1001 => Ok("CantConnectToPeer"),
|
1001 => Ok("CantConnectToPeer"),
|
||||||
_ => {
|
_ => Err(Error(format!("Unknown server message code: {code}"))),
|
||||||
Err(Error(format!("Unknown server message code: {}", code)))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
MessageType::PeerInit => match code {
|
MessageType::PeerInit => match code {
|
||||||
0 => Ok("PierceFireWall"),
|
0 => Ok("PierceFireWall"),
|
||||||
1 => Ok("PeerInit"),
|
1 => Ok("PeerInit"),
|
||||||
_ => Err(Error(format!(
|
_ => Err(Error(format!(
|
||||||
"Unknown peer init message code: {}",
|
"Unknown peer init message code: {code}"
|
||||||
code
|
|
||||||
))),
|
))),
|
||||||
},
|
},
|
||||||
MessageType::Peer => match code {
|
MessageType::Peer => match code {
|
||||||
@@ -361,7 +366,7 @@ impl Message {
|
|||||||
46 => Ok("UploadFailed"),
|
46 => Ok("UploadFailed"),
|
||||||
50 => Ok("UploadDenied"),
|
50 => Ok("UploadDenied"),
|
||||||
51 => Ok("PlaceInQueueRequest"),
|
51 => Ok("PlaceInQueueRequest"),
|
||||||
_ => Err(Error(format!("Unknown peer message code: {}", code))),
|
_ => Err(Error(format!("Unknown peer message code: {code}"))),
|
||||||
},
|
},
|
||||||
MessageType::Distributed => match code {
|
MessageType::Distributed => match code {
|
||||||
3 => Ok("SearchRequest"),
|
3 => Ok("SearchRequest"),
|
||||||
@@ -369,8 +374,7 @@ impl Message {
|
|||||||
5 => Ok("BranchRoot"),
|
5 => Ok("BranchRoot"),
|
||||||
93 => Ok("EmbeddedMessage"),
|
93 => Ok("EmbeddedMessage"),
|
||||||
_ => Err(Error(format!(
|
_ => Err(Error(format!(
|
||||||
"Unknown distributed message code: {}",
|
"Unknown distributed message code: {code}"
|
||||||
code
|
|
||||||
))),
|
))),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ impl MessageHandler<PeerMessage> for FileSearchResponse {
|
|||||||
9
|
9
|
||||||
}
|
}
|
||||||
fn handle(&self, message: &mut Message, sender: Sender<PeerMessage>) {
|
fn handle(&self, message: &mut Message, sender: Sender<PeerMessage>) {
|
||||||
let file_search = match SearchResult::new_from_message(message) {
|
// Skip malformed search results
|
||||||
Ok(result) => result,
|
let Ok(file_search) = SearchResult::new_from_message(message) else {
|
||||||
Err(_) => return, // Skip malformed search results
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
sender
|
sender
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl MessageHandler<ServerMessage> for GetPeerAddressHandler {
|
|||||||
let port = message.read_int32();
|
let port = message.read_int32();
|
||||||
let obfuscation_type = message.read_int32();
|
let obfuscation_type = message.read_int32();
|
||||||
let obfuscated_port = message.read_int32() as u16;
|
let obfuscated_port = message.read_int32() as u16;
|
||||||
println!("GetPeerAddressHandler: {:?}", username); // Debug print
|
println!("GetPeerAddressHandler: {username:?}"); // Debug print
|
||||||
|
|
||||||
sender
|
sender
|
||||||
.send(ServerMessage::GetPeerAddressResponse {
|
.send(ServerMessage::GetPeerAddressResponse {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
|
|
||||||
pub struct MessageFactory;
|
pub struct MessageFactory;
|
||||||
impl MessageFactory {
|
impl MessageFactory {
|
||||||
|
#[must_use]
|
||||||
pub fn build_get_peer_address(username: &str) -> Message {
|
pub fn build_get_peer_address(username: &str) -> Message {
|
||||||
let mut message = Message::new();
|
let mut message = Message::new();
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ impl MessageFactory {
|
|||||||
message.write_string(username);
|
message.write_string(username);
|
||||||
message
|
message
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn build_login_message(username: &str, password: &str) -> Message {
|
pub fn build_login_message(username: &str, password: &str) -> Message {
|
||||||
// Message::new_with_data(
|
// Message::new_with_data(
|
||||||
// [
|
// [
|
||||||
@@ -42,6 +44,7 @@ impl MessageFactory {
|
|||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn build_shared_folders_message(
|
pub fn build_shared_folders_message(
|
||||||
folder_count: u32,
|
folder_count: u32,
|
||||||
file_count: u32,
|
file_count: u32,
|
||||||
@@ -52,6 +55,7 @@ impl MessageFactory {
|
|||||||
.write_int32(file_count)
|
.write_int32(file_count)
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn build_file_search_message(token: u32, query: &str) -> Message {
|
pub fn build_file_search_message(token: u32, query: &str) -> Message {
|
||||||
Message::new()
|
Message::new()
|
||||||
.write_int32(26)
|
.write_int32(26)
|
||||||
@@ -59,21 +63,25 @@ impl MessageFactory {
|
|||||||
.write_string(query)
|
.write_string(query)
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn build_set_status_message(status_code: u32) -> Message {
|
pub fn build_set_status_message(status_code: u32) -> Message {
|
||||||
Message::new()
|
Message::new()
|
||||||
.write_int32(28)
|
.write_int32(28)
|
||||||
.write_int32(status_code)
|
.write_int32(status_code)
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn build_no_parent_message() -> Message {
|
pub fn build_no_parent_message() -> Message {
|
||||||
Message::new().write_int32(71).write_bool(true).clone()
|
Message::new().write_int32(71).write_bool(true).clone()
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn build_set_wait_port_message(port: u16) -> Message {
|
pub fn build_set_wait_port_message(port: u16) -> Message {
|
||||||
Message::new()
|
Message::new()
|
||||||
.write_int32(2)
|
.write_int32(2)
|
||||||
.write_int32(port.into())
|
.write_int32(port.into())
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn build_watch_user(token: u32) -> Message {
|
pub fn build_watch_user(token: u32) -> Message {
|
||||||
Message::new()
|
Message::new()
|
||||||
.write_raw_bytes([5, 0, 0, 0, 0].to_vec())
|
.write_raw_bytes([5, 0, 0, 0, 0].to_vec())
|
||||||
@@ -81,6 +89,7 @@ impl MessageFactory {
|
|||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn build_queue_upload_message(filename: &str) -> Message {
|
pub fn build_queue_upload_message(filename: &str) -> Message {
|
||||||
Message::new()
|
Message::new()
|
||||||
.write_int32(43)
|
.write_int32(43)
|
||||||
@@ -88,6 +97,7 @@ impl MessageFactory {
|
|||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn build_transfer_request_message(
|
pub fn build_transfer_request_message(
|
||||||
filename: &str,
|
filename: &str,
|
||||||
token: u32,
|
token: u32,
|
||||||
@@ -99,6 +109,7 @@ impl MessageFactory {
|
|||||||
.write_string(filename)
|
.write_string(filename)
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn build_transfer_response_message(transfer: Transfer) -> Message {
|
pub fn build_transfer_response_message(transfer: Transfer) -> Message {
|
||||||
Message::new()
|
Message::new()
|
||||||
.write_int32(41)
|
.write_int32(41)
|
||||||
@@ -106,6 +117,7 @@ impl MessageFactory {
|
|||||||
.write_bool(true)
|
.write_bool(true)
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
pub fn build_pierce_firewall_message(token: u32) -> Message {
|
pub fn build_pierce_firewall_message(token: u32) -> Message {
|
||||||
Message::new()
|
Message::new()
|
||||||
.write_int8(0) // PierceFirewall message code
|
.write_int8(0) // PierceFirewall message code
|
||||||
@@ -114,6 +126,7 @@ impl MessageFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[must_use]
|
||||||
pub fn build_peer_init_message(
|
pub fn build_peer_init_message(
|
||||||
own_username: &str,
|
own_username: &str,
|
||||||
connection_type: ConnectionType,
|
connection_type: ConnectionType,
|
||||||
@@ -134,7 +147,7 @@ fn test_build_watch_user() {
|
|||||||
let message = MessageFactory::build_watch_user(token);
|
let message = MessageFactory::build_watch_user(token);
|
||||||
let expect: Vec<u8> = [5, 0, 0, 0, 0, 223, 0, 0, 0].to_vec();
|
let expect: Vec<u8> = [5, 0, 0, 0, 0, 223, 0, 0, 0].to_vec();
|
||||||
|
|
||||||
assert_eq!(expect, message.get_data())
|
assert_eq!(expect, message.get_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -151,7 +164,7 @@ fn test_build_login_message() {
|
|||||||
]
|
]
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
assert_eq!(expect, message.get_data())
|
assert_eq!(expect, message.get_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -162,5 +175,5 @@ fn test_build_file_search_message() {
|
|||||||
119, 97, 120,
|
119, 97, 120,
|
||||||
]
|
]
|
||||||
.to_vec();
|
.to_vec();
|
||||||
assert_eq!(expect, message.get_data())
|
assert_eq!(expect, message.get_data());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ impl MessageHandler<ServerMessage> for MessageUser {
|
|||||||
let user_message = UserMessage::new(
|
let user_message = UserMessage::new(
|
||||||
id,
|
id,
|
||||||
timestamp,
|
timestamp,
|
||||||
username.clone(),
|
username,
|
||||||
message_content,
|
message_content,
|
||||||
new_message,
|
new_message,
|
||||||
);
|
);
|
||||||
|
|
||||||
info!("[MessageUser] User message received:{:?}", user_message);
|
info!("[MessageUser] User message received:{:?}", user_message);
|
||||||
user_message.print()
|
user_message.print();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,22 +37,22 @@ pub enum DownloadError {
|
|||||||
impl std::fmt::Display for DownloadError {
|
impl std::fmt::Display for DownloadError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::ConnectionFailed(e) => write!(f, "Connection failed: {}", e),
|
Self::ConnectionFailed(e) => write!(f, "Connection failed: {e}"),
|
||||||
Self::InvalidAddress(addr) => {
|
Self::InvalidAddress(addr) => {
|
||||||
write!(f, "Invalid address: {}", addr)
|
write!(f, "Invalid address: {addr}")
|
||||||
}
|
}
|
||||||
Self::HandshakeFailed(e) => write!(f, "Handshake failed: {}", e),
|
Self::HandshakeFailed(e) => write!(f, "Handshake failed: {e}"),
|
||||||
Self::StreamReadError(e) => write!(f, "Stream read error: {}", e),
|
Self::StreamReadError(e) => write!(f, "Stream read error: {e}"),
|
||||||
Self::StreamWriteError(e) => write!(f, "Stream write error: {}", e),
|
Self::StreamWriteError(e) => write!(f, "Stream write error: {e}"),
|
||||||
Self::TokenNotFound(token) => {
|
Self::TokenNotFound(token) => {
|
||||||
write!(f, "Token not found: {}", token)
|
write!(f, "Token not found: {token}")
|
||||||
}
|
}
|
||||||
Self::DownloadInfoMissing(token) => {
|
Self::DownloadInfoMissing(token) => {
|
||||||
write!(f, "Download info missing for token: {}", token)
|
write!(f, "Download info missing for token: {token}")
|
||||||
}
|
}
|
||||||
Self::FileWriteError(e) => write!(f, "File write error: {}", e),
|
Self::FileWriteError(e) => write!(f, "File write error: {e}"),
|
||||||
Self::PathResolutionError(msg) => {
|
Self::PathResolutionError(msg) => {
|
||||||
write!(f, "Path resolution error: {}", msg)
|
write!(f, "Path resolution error: {msg}")
|
||||||
}
|
}
|
||||||
Self::InvalidTokenBytes => {
|
Self::InvalidTokenBytes => {
|
||||||
write!(f, "Invalid token bytes received")
|
write!(f, "Invalid token bytes received")
|
||||||
@@ -100,7 +100,7 @@ struct StreamProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StreamProcessor {
|
impl StreamProcessor {
|
||||||
fn new() -> Self {
|
const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
total_bytes: 0,
|
total_bytes: 0,
|
||||||
received: false,
|
received: false,
|
||||||
@@ -113,7 +113,7 @@ impl StreamProcessor {
|
|||||||
self.total_bytes += data.len();
|
self.total_bytes += data.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_continue(&self, expected_size: Option<usize>) -> bool {
|
const fn should_continue(&self, expected_size: Option<usize>) -> bool {
|
||||||
if let Some(size) = expected_size {
|
if let Some(size) = expected_size {
|
||||||
self.total_bytes < size
|
self.total_bytes < size
|
||||||
} else {
|
} else {
|
||||||
@@ -134,7 +134,7 @@ pub struct DownloadPeer {
|
|||||||
|
|
||||||
impl DownloadPeer {
|
impl DownloadPeer {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(
|
pub const fn new(
|
||||||
username: String,
|
username: String,
|
||||||
host: String,
|
host: String,
|
||||||
port: u32,
|
port: u32,
|
||||||
@@ -277,7 +277,7 @@ impl DownloadPeer {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref dl) = download {
|
if let Some(ref dl) = download {
|
||||||
self.wait_while_paused(client_context, dl)?;
|
Self::wait_while_paused(client_context, dl)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
match stream.read(&mut read_buffer) {
|
match stream.read(&mut read_buffer) {
|
||||||
@@ -387,7 +387,6 @@ impl DownloadPeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_while_paused(
|
fn wait_while_paused(
|
||||||
&self,
|
|
||||||
client_context: &Arc<RwLock<ClientContext>>,
|
client_context: &Arc<RwLock<ClientContext>>,
|
||||||
download: &Download,
|
download: &Download,
|
||||||
) -> Result<(), DownloadError> {
|
) -> Result<(), DownloadError> {
|
||||||
@@ -408,7 +407,6 @@ impl DownloadPeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_download_path(
|
fn resolve_download_path(
|
||||||
&self,
|
|
||||||
download: &Download,
|
download: &Download,
|
||||||
) -> Result<String, DownloadError> {
|
) -> Result<String, DownloadError> {
|
||||||
let download_directory = &download.download_directory;
|
let download_directory = &download.download_directory;
|
||||||
@@ -462,7 +460,6 @@ impl DownloadPeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save_downloaded_file(
|
fn save_downloaded_file(
|
||||||
&self,
|
|
||||||
path: &str,
|
path: &str,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> Result<(), DownloadError> {
|
) -> Result<(), DownloadError> {
|
||||||
@@ -511,8 +508,8 @@ impl DownloadPeer {
|
|||||||
let (buffer, download) =
|
let (buffer, download) =
|
||||||
self.read_download_stream(&mut stream, &client_context, download)?;
|
self.read_download_stream(&mut stream, &client_context, download)?;
|
||||||
|
|
||||||
let final_path = self.resolve_download_path(&download)?;
|
let final_path = Self::resolve_download_path(&download)?;
|
||||||
self.save_downloaded_file(&final_path, &buffer)?;
|
Self::save_downloaded_file(&final_path, &buffer)?;
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"[download_peer:{}] download completed successfully: {} bytes, saved to: {}",
|
"[download_peer:{}] download completed successfully: {} bytes, saved to: {}",
|
||||||
@@ -533,7 +530,8 @@ mod tests {
|
|||||||
fn finalize_rejects_truncated_download() {
|
fn finalize_rejects_truncated_download() {
|
||||||
// Peer closed early: 5 of 10 promised bytes. Must be a failure so the
|
// Peer closed early: 5 of 10 promised bytes. Must be a failure so the
|
||||||
// partial file is never reported as Completed.
|
// partial file is never reported as Completed.
|
||||||
let result = DownloadPeer::finalize_download_buffer(vec![1, 2, 3, 4, 5], 10);
|
let result =
|
||||||
|
DownloadPeer::finalize_download_buffer(vec![1, 2, 3, 4, 5], 10);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
result,
|
result,
|
||||||
Err(DownloadError::IncompleteDownload {
|
Err(DownloadError::IncompleteDownload {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ fn parse_token_from_buffer(buffer: &[u8], username: &str) -> Option<u32> {
|
|||||||
let token = u32::from_le_bytes(
|
let token = u32::from_le_bytes(
|
||||||
token_bytes
|
token_bytes
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap_or_else(|_| panic!("[listener:{}] slice with incorrect length, can't extract transfer_token", username)),
|
.unwrap_or_else(|_| panic!("[listener:{username}] slice with incorrect length, can't extract transfer_token")),
|
||||||
);
|
);
|
||||||
Some(token)
|
Some(token)
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ fn handle_file_connection(
|
|||||||
DownloadStatus::Completed,
|
DownloadStatus::Completed,
|
||||||
),
|
),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("[listener] handle_file_connection write: {}", e)
|
error!("[listener] handle_file_connection write: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
@@ -204,7 +204,10 @@ fn handle_file_connection(
|
|||||||
DownloadStatus::Failed,
|
DownloadStatus::Failed,
|
||||||
),
|
),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("[listener] handle_file_connection fail write: {}", e)
|
error!(
|
||||||
|
"[listener] handle_file_connection fail write: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,7 +275,7 @@ fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
|
|||||||
&context,
|
&context,
|
||||||
&peer_ip,
|
&peer_ip,
|
||||||
peer_port,
|
peer_port,
|
||||||
)
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ConnectionType::D => {
|
ConnectionType::D => {
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ impl FromStr for ConnectionType {
|
|||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"P" => Ok(ConnectionType::P),
|
"P" => Ok(Self::P),
|
||||||
"F" => Ok(ConnectionType::F),
|
"F" => Ok(Self::F),
|
||||||
"D" => Ok(ConnectionType::D),
|
"D" => Ok(Self::D),
|
||||||
_ => Err(ParseConnectionTypeError),
|
_ => Err(ParseConnectionTypeError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,11 +71,11 @@ impl FromStr for ConnectionType {
|
|||||||
impl fmt::Display for ConnectionType {
|
impl fmt::Display for ConnectionType {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let s = match self {
|
let s = match self {
|
||||||
ConnectionType::P => "P",
|
Self::P => "P",
|
||||||
ConnectionType::F => "F",
|
Self::F => "F",
|
||||||
ConnectionType::D => "D",
|
Self::D => "D",
|
||||||
};
|
};
|
||||||
write!(f, "{}", s)
|
write!(f, "{s}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +93,8 @@ pub struct Peer {
|
|||||||
}
|
}
|
||||||
impl Peer {
|
impl Peer {
|
||||||
#[allow(clippy::too_many_arguments, dead_code)]
|
#[allow(clippy::too_many_arguments, dead_code)]
|
||||||
pub fn new(
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
username: String,
|
username: String,
|
||||||
connection_type: ConnectionType,
|
connection_type: ConnectionType,
|
||||||
host: String,
|
host: String,
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ pub struct Download {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Download {
|
impl Download {
|
||||||
pub fn is_finished(&self) -> bool {
|
#[must_use]
|
||||||
|
pub const fn is_finished(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.status,
|
self.status,
|
||||||
DownloadStatus::Completed
|
DownloadStatus::Completed
|
||||||
@@ -118,7 +119,8 @@ impl Download {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bytes_downloaded(&self) -> u64 {
|
#[must_use]
|
||||||
|
pub const fn bytes_downloaded(&self) -> u64 {
|
||||||
match &self.status {
|
match &self.status {
|
||||||
DownloadStatus::InProgress {
|
DownloadStatus::InProgress {
|
||||||
bytes_downloaded, ..
|
bytes_downloaded, ..
|
||||||
@@ -131,7 +133,8 @@ impl Download {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn speed_bytes_per_sec(&self) -> f64 {
|
#[must_use]
|
||||||
|
pub const fn speed_bytes_per_sec(&self) -> f64 {
|
||||||
match &self.status {
|
match &self.status {
|
||||||
DownloadStatus::InProgress {
|
DownloadStatus::InProgress {
|
||||||
speed_bytes_per_sec,
|
speed_bytes_per_sec,
|
||||||
|
|||||||
@@ -32,12 +32,10 @@ pub fn init() {
|
|||||||
unsafe {
|
unsafe {
|
||||||
LOG_LEVEL = match level.as_str() {
|
LOG_LEVEL = match level.as_str() {
|
||||||
"ERROR" => LogLevel::Error,
|
"ERROR" => LogLevel::Error,
|
||||||
"WARN" => LogLevel::Warn,
|
|
||||||
"INFO" => LogLevel::Info,
|
"INFO" => LogLevel::Info,
|
||||||
"DEBUG" => LogLevel::Debug,
|
"DEBUG" | "VERBOSE" => LogLevel::Debug, // Map VERBOSE to DEBUG
|
||||||
"TRACE" => LogLevel::Trace,
|
"TRACE" => LogLevel::Trace,
|
||||||
"VERBOSE" => LogLevel::Debug, // Map VERBOSE to DEBUG
|
_ => LogLevel::Warn, // "WARN" or default
|
||||||
_ => LogLevel::Warn, // Default to WARN
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +52,7 @@ pub fn init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
eprintln!("Failed to open log file '{log_file_path}': {e}");
|
||||||
"Failed to open log file '{}': {}",
|
|
||||||
log_file_path, e
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +69,7 @@ enum LogSink {
|
|||||||
Stderr,
|
Stderr,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn choose_sink(buffering: bool, has_file: bool) -> LogSink {
|
const fn choose_sink(buffering: bool, has_file: bool) -> LogSink {
|
||||||
match (buffering, has_file) {
|
match (buffering, has_file) {
|
||||||
// A configured file always takes the line directly and bypasses
|
// A configured file always takes the line directly and bypasses
|
||||||
// buffering, so the flush step never re-writes it.
|
// buffering, so the flush step never re-writes it.
|
||||||
@@ -86,7 +81,7 @@ fn choose_sink(buffering: bool, has_file: bool) -> LogSink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn has_log_file() -> bool {
|
fn has_log_file() -> bool {
|
||||||
LOG_FILE.lock().map(|f| f.is_some()).unwrap_or(false)
|
LOG_FILE.lock().is_ok_and(|f| f.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log(level: LogLevel, message: &str) {
|
pub fn log(level: LogLevel, message: &str) {
|
||||||
@@ -161,29 +156,11 @@ pub fn log(level: LogLevel, message: &str) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let formatted_message = format!(
|
let formatted_message = format!(
|
||||||
"[{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}] [{}] {}",
|
"[{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{subsec_millis:03}] [{level_str}] {message}"
|
||||||
year,
|
|
||||||
month,
|
|
||||||
day,
|
|
||||||
hours,
|
|
||||||
minutes,
|
|
||||||
seconds,
|
|
||||||
subsec_millis,
|
|
||||||
level_str,
|
|
||||||
message
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let formatted_message_plain = format!(
|
let formatted_message_plain = format!(
|
||||||
"[{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}] [{}] {}",
|
"[{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{subsec_millis:03}] [{level_str_plain}] {message}"
|
||||||
year,
|
|
||||||
month,
|
|
||||||
day,
|
|
||||||
hours,
|
|
||||||
minutes,
|
|
||||||
seconds,
|
|
||||||
subsec_millis,
|
|
||||||
level_str_plain,
|
|
||||||
message
|
|
||||||
);
|
);
|
||||||
|
|
||||||
match choose_sink(BUFFERING.load(Ordering::Relaxed), has_log_file())
|
match choose_sink(BUFFERING.load(Ordering::Relaxed), has_log_file())
|
||||||
@@ -192,18 +169,17 @@ pub fn log(level: LogLevel, message: &str) {
|
|||||||
if let Ok(mut log_file) = LOG_FILE.lock()
|
if let Ok(mut log_file) = LOG_FILE.lock()
|
||||||
&& let Some(file) = log_file.as_mut()
|
&& let Some(file) = log_file.as_mut()
|
||||||
{
|
{
|
||||||
let _ =
|
let _ = writeln!(file, "{formatted_message_plain}");
|
||||||
writeln!(file, "{}", formatted_message_plain);
|
|
||||||
let _ = file.flush();
|
let _ = file.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogSink::Buffer => {
|
LogSink::Buffer => {
|
||||||
if let Ok(mut buffer) = BUFFER.lock() {
|
if let Ok(mut buffer) = BUFFER.lock() {
|
||||||
buffer.push(formatted_message.clone());
|
buffer.push(formatted_message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogSink::Stderr => {
|
LogSink::Stderr => {
|
||||||
eprintln!("{}", formatted_message);
|
eprintln!("{formatted_message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,18 +204,18 @@ pub fn flush_buffered_logs() {
|
|||||||
for message in buffer.iter() {
|
for message in buffer.iter() {
|
||||||
// Strip ANSI codes for file output
|
// Strip ANSI codes for file output
|
||||||
let plain_message = strip_ansi_codes(message);
|
let plain_message = strip_ansi_codes(message);
|
||||||
let _ = writeln!(file, "{}", plain_message);
|
let _ = writeln!(file, "{plain_message}");
|
||||||
}
|
}
|
||||||
let _ = file.flush();
|
let _ = file.flush();
|
||||||
} else {
|
} else {
|
||||||
for message in buffer.iter() {
|
for message in buffer.iter() {
|
||||||
eprintln!("{}", message);
|
eprintln!("{message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If lock fails, fall back to stderr
|
// If lock fails, fall back to stderr
|
||||||
for message in buffer.iter() {
|
for message in buffer.iter() {
|
||||||
eprintln!("{}", message);
|
eprintln!("{message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
@@ -270,26 +246,6 @@ fn strip_ansi_codes(s: &str) -> String {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{LogSink, choose_sink};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_configured_file_bypasses_buffering_so_lines_are_not_duplicated() {
|
|
||||||
// With a file configured, the line goes straight to the file whether or
|
|
||||||
// not buffering is on, so it is never also queued for a flush that would
|
|
||||||
// write it a second time.
|
|
||||||
assert_eq!(choose_sink(true, true), LogSink::File);
|
|
||||||
assert_eq!(choose_sink(false, true), LogSink::File);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn without_a_file_buffering_routes_to_the_buffer_else_stderr() {
|
|
||||||
assert_eq!(choose_sink(true, false), LogSink::Buffer);
|
|
||||||
assert_eq!(choose_sink(false, false), LogSink::Stderr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! error {
|
macro_rules! error {
|
||||||
($($arg:tt)*) => {
|
($($arg:tt)*) => {
|
||||||
@@ -324,3 +280,23 @@ macro_rules! trace {
|
|||||||
$crate::utils::logger::log($crate::utils::logger::LogLevel::Trace, &format!($($arg)*))
|
$crate::utils::logger::log($crate::utils::logger::LogLevel::Trace, &format!($($arg)*))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{LogSink, choose_sink};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_configured_file_bypasses_buffering_so_lines_are_not_duplicated() {
|
||||||
|
// With a file configured, the line goes straight to the file whether or
|
||||||
|
// not buffering is on, so it is never also queued for a flush that would
|
||||||
|
// write it a second time.
|
||||||
|
assert_eq!(choose_sink(true, true), LogSink::File);
|
||||||
|
assert_eq!(choose_sink(false, true), LogSink::File);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn without_a_file_buffering_routes_to_the_buffer_else_stderr() {
|
||||||
|
assert_eq!(choose_sink(true, false), LogSink::Buffer);
|
||||||
|
assert_eq!(choose_sink(false, false), LogSink::Stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use std::convert::TryInto;
|
|||||||
* K[i] = floor(2^32 * abs(sin(i+1)))
|
* K[i] = floor(2^32 * abs(sin(i+1)))
|
||||||
*/
|
*/
|
||||||
fn table_construction_function(i: u32) -> u32 {
|
fn table_construction_function(i: u32) -> u32 {
|
||||||
let x: f64 = i as f64;
|
let x: f64 = f64::from(i);
|
||||||
let sin_eval = x.sin().abs();
|
let sin_eval = x.sin().abs();
|
||||||
// note: 4294967296 == 2^32
|
// note: 4294967296 == 2^32
|
||||||
(4294967296.0 * sin_eval) as u32
|
(4294967296.0 * sin_eval) as u32
|
||||||
@@ -36,19 +36,19 @@ fn table_construction_function(i: u32) -> u32 {
|
|||||||
* algorithm, and they are performed a number of times to end
|
* algorithm, and they are performed a number of times to end
|
||||||
* up with our final result.
|
* up with our final result.
|
||||||
*/
|
*/
|
||||||
fn f(x: u32, y: u32, z: u32) -> u32 {
|
const fn f(x: u32, y: u32, z: u32) -> u32 {
|
||||||
x & y | !x & z
|
x & y | !x & z
|
||||||
}
|
}
|
||||||
|
|
||||||
fn g(x: u32, y: u32, z: u32) -> u32 {
|
const fn g(x: u32, y: u32, z: u32) -> u32 {
|
||||||
x & z | y & !z
|
x & z | y & !z
|
||||||
}
|
}
|
||||||
|
|
||||||
fn h(x: u32, y: u32, z: u32) -> u32 {
|
const fn h(x: u32, y: u32, z: u32) -> u32 {
|
||||||
x ^ y ^ z
|
x ^ y ^ z
|
||||||
}
|
}
|
||||||
|
|
||||||
fn i(x: u32, y: u32, z: u32) -> u32 {
|
const fn i(x: u32, y: u32, z: u32) -> u32 {
|
||||||
y ^ (x | !z)
|
y ^ (x | !z)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ fn vec_to_array<T, const N: usize>(v: Vec<T>) -> [T; N] {
|
|||||||
* table[i] is an entry generated by our sine function, s is an unsigned
|
* table[i] is an entry generated by our sine function, s is an unsigned
|
||||||
* integer, and a,b,c,d are our 32 bit registers we perform operations on.
|
* integer, and a,b,c,d are our 32 bit registers we perform operations on.
|
||||||
*/
|
*/
|
||||||
fn round_one_operations(
|
const fn round_one_operations(
|
||||||
mut a: u32,
|
mut a: u32,
|
||||||
mut b: u32,
|
mut b: u32,
|
||||||
mut c: u32,
|
mut c: u32,
|
||||||
@@ -115,7 +115,7 @@ fn round_one_operations(
|
|||||||
* Round 2 is equivalent to round 1, except we swap out our f function for g as such:
|
* Round 2 is equivalent to round 1, except we swap out our f function for g as such:
|
||||||
* a = b + ((a + g(b,c,d) + x[k] + table[i]) <<< s);
|
* a = b + ((a + g(b,c,d) + x[k] + table[i]) <<< s);
|
||||||
*/
|
*/
|
||||||
fn round_two_operations(
|
const fn round_two_operations(
|
||||||
mut a: u32,
|
mut a: u32,
|
||||||
mut b: u32,
|
mut b: u32,
|
||||||
mut c: u32,
|
mut c: u32,
|
||||||
@@ -161,7 +161,7 @@ fn round_two_operations(
|
|||||||
* Round 3 is equivalent to round 1, except we swap out our f function for h as such:
|
* Round 3 is equivalent to round 1, except we swap out our f function for h as such:
|
||||||
* a = b + ((a + h(b,c,d) + x[k] + table[i]) <<< s);
|
* a = b + ((a + h(b,c,d) + x[k] + table[i]) <<< s);
|
||||||
*/
|
*/
|
||||||
fn round_three_operations(
|
const fn round_three_operations(
|
||||||
mut a: u32,
|
mut a: u32,
|
||||||
mut b: u32,
|
mut b: u32,
|
||||||
mut c: u32,
|
mut c: u32,
|
||||||
@@ -207,7 +207,7 @@ fn round_three_operations(
|
|||||||
* Round 4 is equivalent to round 1, except we swap out our f function for h as such:
|
* Round 4 is equivalent to round 1, except we swap out our f function for h as such:
|
||||||
* a = b + ((a + i(b,c,d) + x[k] + table[i]) <<< s);
|
* a = b + ((a + i(b,c,d) + x[k] + table[i]) <<< s);
|
||||||
*/
|
*/
|
||||||
fn round_four_operations(
|
const fn round_four_operations(
|
||||||
mut a: u32,
|
mut a: u32,
|
||||||
mut b: u32,
|
mut b: u32,
|
||||||
mut c: u32,
|
mut c: u32,
|
||||||
@@ -287,7 +287,7 @@ fn compute_md5_digest(mut v: Vec<u8>) -> String {
|
|||||||
let table = construct_value_table();
|
let table = construct_value_table();
|
||||||
|
|
||||||
// let M[0 .. N-1] = words of resulting message, where N is multiple of 16
|
// let M[0 .. N-1] = words of resulting message, where N is multiple of 16
|
||||||
for chunk in v.chunks_exact_mut(64) {
|
for chunk in v.as_chunks_mut::<64>().0 {
|
||||||
let x = convert_u8_chunk_to_u32(chunk);
|
let x = convert_u8_chunk_to_u32(chunk);
|
||||||
|
|
||||||
// set all values of a,b,c,d to aa,bb,cc, and dd respectively.
|
// set all values of a,b,c,d to aa,bb,cc, and dd respectively.
|
||||||
@@ -378,7 +378,7 @@ fn bit_padding(input: &str) -> Vec<u8> {
|
|||||||
input_vector
|
input_vector
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_u64_to_u8_array(s: u64) -> [u8; 8] {
|
const fn split_u64_to_u8_array(s: u64) -> [u8; 8] {
|
||||||
[
|
[
|
||||||
s as u8,
|
s as u8,
|
||||||
(s >> 8) as u8,
|
(s >> 8) as u8,
|
||||||
@@ -425,6 +425,7 @@ fn convert_str_to_vec(input: &str) -> Vec<u8> {
|
|||||||
* during our bitwise operations to retrieve our message digest.
|
* during our bitwise operations to retrieve our message digest.
|
||||||
* (This is a simplification, but describes the core functionality)
|
* (This is a simplification, but describes the core functionality)
|
||||||
*/
|
*/
|
||||||
|
#[must_use]
|
||||||
pub fn md5(input: &str) -> String {
|
pub fn md5(input: &str) -> String {
|
||||||
let input_vec = bit_padding(input);
|
let input_vec = bit_padding(input);
|
||||||
compute_md5_digest(input_vec)
|
compute_md5_digest(input_vec)
|
||||||
@@ -454,7 +455,7 @@ fn lowercase_alphabet_correct_hash() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
"c3fcd3d76192e4007dfb496cca67e13b",
|
"c3fcd3d76192e4007dfb496cca67e13b",
|
||||||
md5("abcdefghijklmnopqrstuvwxyz")
|
md5("abcdefghijklmnopqrstuvwxyz")
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn expand_tilde(path: &str) -> PathBuf {
|
pub fn expand_tilde(path: &str) -> PathBuf {
|
||||||
if let Some(stripped) = path.strip_prefix('~')
|
if let Some(stripped) = path.strip_prefix('~')
|
||||||
&& let Ok(home) = std::env::var("HOME")
|
&& let Ok(home) = std::env::var("HOME")
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ pub struct ThreadPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ThreadPool {
|
impl ThreadPool {
|
||||||
pub fn new(size: usize) -> ThreadPool {
|
#[must_use]
|
||||||
|
pub fn new(size: usize) -> Self {
|
||||||
assert!(size > 0);
|
assert!(size > 0);
|
||||||
|
|
||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
@@ -27,7 +28,7 @@ impl ThreadPool {
|
|||||||
workers.push(Worker::new(Arc::clone(&receiver)));
|
workers.push(Worker::new(Arc::clone(&receiver)));
|
||||||
}
|
}
|
||||||
|
|
||||||
ThreadPool {
|
Self {
|
||||||
workers,
|
workers,
|
||||||
sender: Some(sender),
|
sender: Some(sender),
|
||||||
}
|
}
|
||||||
@@ -62,6 +63,39 @@ impl Drop for ThreadPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Worker {
|
||||||
|
thread: Option<thread::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Worker {
|
||||||
|
fn new(receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Self {
|
||||||
|
let thread = thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
let message = match receiver.lock_safe() {
|
||||||
|
Ok(rx) => rx.recv(),
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
match message {
|
||||||
|
// Contain a panicking job so it kills only that job, not the
|
||||||
|
// worker: the pool has a fixed number of workers and a lost
|
||||||
|
// one is never replaced. The lock is already released here,
|
||||||
|
// so a panic cannot poison the shared receiver.
|
||||||
|
Ok(Message::NewJob(job)) => {
|
||||||
|
let _ = std::panic::catch_unwind(
|
||||||
|
std::panic::AssertUnwindSafe(job),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Message::Terminate) | Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
thread: Some(thread),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::ThreadPool;
|
use super::ThreadPool;
|
||||||
@@ -88,36 +122,3 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Worker {
|
|
||||||
thread: Option<thread::JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Worker {
|
|
||||||
fn new(receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
|
|
||||||
let thread = thread::spawn(move || {
|
|
||||||
loop {
|
|
||||||
let message = match receiver.lock_safe() {
|
|
||||||
Ok(rx) => rx.recv(),
|
|
||||||
Err(_) => break,
|
|
||||||
};
|
|
||||||
match message {
|
|
||||||
// Contain a panicking job so it kills only that job, not the
|
|
||||||
// worker: the pool has a fixed number of workers and a lost
|
|
||||||
// one is never replaced. The lock is already released here,
|
|
||||||
// so a panic cannot poison the shared receiver.
|
|
||||||
Ok(Message::NewJob(job)) => {
|
|
||||||
let _ = std::panic::catch_unwind(
|
|
||||||
std::panic::AssertUnwindSafe(job),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(Message::Terminate) | Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Worker {
|
|
||||||
thread: Some(thread),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ struct BitReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BitReader {
|
impl BitReader {
|
||||||
fn new(mem: Vec<u8>) -> Self {
|
const fn new(mem: Vec<u8>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mem,
|
mem,
|
||||||
pos: 0,
|
pos: 0,
|
||||||
@@ -42,7 +42,7 @@ impl BitReader {
|
|||||||
fn read_bits(&mut self, n: usize) -> std::result::Result<u32, String> {
|
fn read_bits(&mut self, n: usize) -> std::result::Result<u32, String> {
|
||||||
let mut o = 0u32;
|
let mut o = 0u32;
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
o |= (self.read_bit()? as u32) << i;
|
o |= u32::from(self.read_bit()?) << i;
|
||||||
}
|
}
|
||||||
Ok(o)
|
Ok(o)
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ impl BitReader {
|
|||||||
// read bytes as an integer in little-endian
|
// read bytes as an integer in little-endian
|
||||||
let mut o = 0u32;
|
let mut o = 0u32;
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
o |= (self.read_byte()? as u32) << (8 * i);
|
o |= u32::from(self.read_byte()?) << (8 * i);
|
||||||
}
|
}
|
||||||
Ok(o)
|
Ok(o)
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ pub fn deflate(input: &[u8]) -> Result<Vec<u8>> {
|
|||||||
return Err(SoulseekRs::CompressionError("invalid CINFO".to_string()));
|
return Err(SoulseekRs::CompressionError("invalid CINFO".to_string()));
|
||||||
}
|
}
|
||||||
let flg = r.read_byte()?;
|
let flg = r.read_byte()?;
|
||||||
if !((cmf as u32) * 256 + (flg as u32)).is_multiple_of(31) {
|
if !(u32::from(cmf) * 256 + u32::from(flg)).is_multiple_of(31) {
|
||||||
return Err(SoulseekRs::CompressionError(
|
return Err(SoulseekRs::CompressionError(
|
||||||
"CMF+FLG checksum failed".to_string(),
|
"CMF+FLG checksum failed".to_string(),
|
||||||
));
|
));
|
||||||
@@ -119,12 +119,12 @@ fn inflate_block_no_compression(
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Node {
|
struct Node {
|
||||||
symbol: Option<u32>,
|
symbol: Option<u32>,
|
||||||
left: Option<Box<Node>>,
|
left: Option<Box<Self>>,
|
||||||
right: Option<Box<Node>>,
|
right: Option<Box<Self>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
fn new() -> Self {
|
const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
symbol: None,
|
symbol: None,
|
||||||
left: None,
|
left: None,
|
||||||
@@ -138,7 +138,7 @@ struct HuffmanTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HuffmanTree {
|
impl HuffmanTree {
|
||||||
fn new() -> Self {
|
const fn new() -> Self {
|
||||||
Self { root: Node::new() }
|
Self { root: Node::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,9 +175,9 @@ fn decode_symbol(
|
|||||||
// at a missing child; error out instead of unwrap-panicking.
|
// at a missing child; error out instead of unwrap-panicking.
|
||||||
node = next
|
node = next
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or("Invalid Huffman code".to_string())?;
|
.ok_or_else(|| "Invalid Huffman code".to_string())?;
|
||||||
}
|
}
|
||||||
node.symbol.ok_or("No symbol found".to_string())
|
node.symbol.ok_or_else(|| "No symbol found".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
const LENGTH_EXTRA_BITS: [usize; 29] = [
|
const LENGTH_EXTRA_BITS: [usize; 29] = [
|
||||||
@@ -397,7 +397,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extract_header_success() {
|
fn test_extract_header_success() {
|
||||||
let _data = [120, 156]; // Valid zlib header
|
// [120, 156] is a valid zlib header
|
||||||
let result = deflate(&[120, 156, 3, 0, 0, 0, 0, 1]); // Minimal valid zlib stream
|
let result = deflate(&[120, 156, 3, 0, 0, 0, 0, 1]); // Minimal valid zlib stream
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ categories = ["command-line-utilities"]
|
|||||||
name = "soulseek-rs"
|
name = "soulseek-rs"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
soulseek-rs-lib = { version = "0.3.0", path = "../soulseek-rs-lib" }
|
soulseek-rs-lib = { version = "0.3.0", path = "../soulseek-rs-lib" }
|
||||||
|
|
||||||
|
|||||||
+10
-12
@@ -35,7 +35,7 @@ fn main() -> Result<()> {
|
|||||||
if let Some(log_file) = &cli.log_file {
|
if let Some(log_file) = &cli.log_file {
|
||||||
// SAFETY: Called before any threads are spawned
|
// SAFETY: Called before any threads are spawned
|
||||||
unsafe {
|
unsafe {
|
||||||
env::set_var("LOG_FILE", log_file.to_string_lossy().to_string())
|
env::set_var("LOG_FILE", log_file.to_string_lossy().to_string());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,17 +76,20 @@ fn main() -> Result<()> {
|
|||||||
search_and_download(config)
|
search_and_download(config)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
use ratatui::crossterm::{
|
||||||
|
event::EnableMouseCapture,
|
||||||
|
execute,
|
||||||
|
terminal::{Clear, ClearType},
|
||||||
|
};
|
||||||
|
|
||||||
// Launch main TUI
|
// Launch main TUI
|
||||||
// Enable logger buffering BEFORE connection to prevent log artifacts
|
// Enable logger buffering BEFORE connection to prevent log artifacts
|
||||||
soulseek_rs::utils::logger::enable_buffering();
|
soulseek_rs::utils::logger::enable_buffering();
|
||||||
|
|
||||||
let settings = ClientSettings {
|
let settings = ClientSettings {
|
||||||
username: username.clone(),
|
username,
|
||||||
password: password.clone(),
|
password,
|
||||||
server_address: PeerAddress::new(
|
server_address: PeerAddress::new(server_host, server_port),
|
||||||
server_host.clone(),
|
|
||||||
server_port,
|
|
||||||
),
|
|
||||||
enable_listen: !cli.disable_listener,
|
enable_listen: !cli.disable_listener,
|
||||||
listen_port: cli.listener_port,
|
listen_port: cli.listener_port,
|
||||||
};
|
};
|
||||||
@@ -102,11 +105,6 @@ fn main() -> Result<()> {
|
|||||||
let client = Arc::new(client);
|
let client = Arc::new(client);
|
||||||
|
|
||||||
// Clear screen and enable mouse capture before initializing TUI
|
// Clear screen and enable mouse capture before initializing TUI
|
||||||
use ratatui::crossterm::{
|
|
||||||
event::EnableMouseCapture,
|
|
||||||
execute,
|
|
||||||
terminal::{Clear, ClearType},
|
|
||||||
};
|
|
||||||
let _ = execute!(
|
let _ = execute!(
|
||||||
std::io::stdout(),
|
std::io::stdout(),
|
||||||
Clear(ClearType::All),
|
Clear(ClearType::All),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::sync::atomic::AtomicBool;
|
|||||||
use std::sync::{Arc, mpsc::Receiver, mpsc::Sender};
|
use std::sync::{Arc, mpsc::Receiver, mpsc::Sender};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum SearchStatus {
|
pub enum SearchStatus {
|
||||||
Active,
|
Active,
|
||||||
Completed,
|
Completed,
|
||||||
@@ -25,7 +25,7 @@ pub struct DownloadEntry {
|
|||||||
pub receiver: Option<Receiver<DownloadStatus>>,
|
pub receiver: Option<Receiver<DownloadStatus>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum FocusedPane {
|
pub enum FocusedPane {
|
||||||
Searches,
|
Searches,
|
||||||
Results,
|
Results,
|
||||||
|
|||||||
@@ -255,7 +255,10 @@ impl FileSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Previous index with wrap-around, or None when the list is empty.
|
/// Previous index with wrap-around, or None when the list is empty.
|
||||||
fn wrapping_prev(selected: Option<usize>, len: usize) -> Option<usize> {
|
const fn wrapping_prev(
|
||||||
|
selected: Option<usize>,
|
||||||
|
len: usize,
|
||||||
|
) -> Option<usize> {
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -266,7 +269,10 @@ impl FileSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Next index with wrap-around, or None when the list is empty.
|
/// Next index with wrap-around, or None when the list is empty.
|
||||||
fn wrapping_next(selected: Option<usize>, len: usize) -> Option<usize> {
|
const fn wrapping_next(
|
||||||
|
selected: Option<usize>,
|
||||||
|
len: usize,
|
||||||
|
) -> Option<usize> {
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -276,12 +282,12 @@ impl FileSelector {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_previous(&mut self) {
|
const fn select_previous(&mut self) {
|
||||||
let i = Self::wrapping_prev(self.state.selected(), self.items.len());
|
let i = Self::wrapping_prev(self.state.selected(), self.items.len());
|
||||||
self.state.select(i);
|
self.state.select(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_next(&mut self) {
|
const fn select_next(&mut self) {
|
||||||
let i = Self::wrapping_next(self.state.selected(), self.items.len());
|
let i = Self::wrapping_next(self.state.selected(), self.items.len());
|
||||||
self.state.select(i);
|
self.state.select(i);
|
||||||
}
|
}
|
||||||
@@ -290,7 +296,7 @@ impl FileSelector {
|
|||||||
self.state.select((!self.items.is_empty()).then_some(0));
|
self.state.select((!self.items.is_empty()).then_some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_last(&mut self) {
|
const fn select_last(&mut self) {
|
||||||
self.state.select(self.items.len().checked_sub(1));
|
self.state.select(self.items.len().checked_sub(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,9 +401,9 @@ impl FileSelector {
|
|||||||
let is_selected = self.selected_indices.contains(&original_idx);
|
let is_selected = self.selected_indices.contains(&original_idx);
|
||||||
let checkbox = if is_selected { "[✓]" } else { "[ ]" };
|
let checkbox = if is_selected { "[✓]" } else { "[ ]" };
|
||||||
|
|
||||||
let speed_mb = item.speed as f64 / BYTES_PER_MB;
|
let speed_mb = f64::from(item.speed) / BYTES_PER_MB;
|
||||||
let speed_str = if speed_mb > 0.0 {
|
let speed_str = if speed_mb > 0.0 {
|
||||||
format!("{:.1} MB/s", speed_mb)
|
format!("{speed_mb:.1} MB/s")
|
||||||
} else {
|
} else {
|
||||||
"-".to_string()
|
"-".to_string()
|
||||||
};
|
};
|
||||||
@@ -405,7 +411,7 @@ impl FileSelector {
|
|||||||
let slots_str = format!("{}", item.slots);
|
let slots_str = format!("{}", item.slots);
|
||||||
|
|
||||||
let bitrate_str = match item.bitrate {
|
let bitrate_str = match item.bitrate {
|
||||||
Some(br) => format!("{} kbps", br),
|
Some(br) => format!("{br} kbps"),
|
||||||
None => "-".to_string(),
|
None => "-".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -474,7 +480,7 @@ impl FileSelector {
|
|||||||
self.search_start_time.elapsed().as_secs(),
|
self.search_start_time.elapsed().as_secs(),
|
||||||
self.search_timeout.as_secs()
|
self.search_timeout.as_secs()
|
||||||
)),
|
)),
|
||||||
])
|
]);
|
||||||
} else {
|
} else {
|
||||||
loading_message.extend(vec![
|
loading_message.extend(vec![
|
||||||
Span::raw(spinner),
|
Span::raw(spinner),
|
||||||
@@ -482,7 +488,7 @@ impl FileSelector {
|
|||||||
Span::styled(self.soulseek_query.clone(), primary_style()),
|
Span::styled(self.soulseek_query.clone(), primary_style()),
|
||||||
Span::raw("'; No results yet"),
|
Span::raw("'; No results yet"),
|
||||||
]);
|
]);
|
||||||
};
|
}
|
||||||
|
|
||||||
// Center the loading message
|
// Center the loading message
|
||||||
let vertical = Layout::vertical([
|
let vertical = Layout::vertical([
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ impl MultiDownloadProgress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_previous(&mut self) {
|
const fn select_previous(&mut self) {
|
||||||
let i = match self.list_state.selected() {
|
let i = match self.list_state.selected() {
|
||||||
Some(i) if i > 0 => i - 1,
|
Some(i) if i > 0 => i - 1,
|
||||||
_ => self.downloads.len().saturating_sub(1),
|
_ => self.downloads.len().saturating_sub(1),
|
||||||
@@ -163,7 +163,7 @@ impl MultiDownloadProgress {
|
|||||||
self.list_state.select(Some(i));
|
self.list_state.select(Some(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_next(&mut self) {
|
const fn select_next(&mut self) {
|
||||||
let i = match self.list_state.selected() {
|
let i = match self.list_state.selected() {
|
||||||
Some(i) if i < self.downloads.len().saturating_sub(1) => i + 1,
|
Some(i) if i < self.downloads.len().saturating_sub(1) => i + 1,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
@@ -171,11 +171,11 @@ impl MultiDownloadProgress {
|
|||||||
self.list_state.select(Some(i));
|
self.list_state.select(Some(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_first(&mut self) {
|
const fn select_first(&mut self) {
|
||||||
self.list_state.select(Some(0));
|
self.list_state.select(Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_last(&mut self) {
|
const fn select_last(&mut self) {
|
||||||
if !self.downloads.is_empty() {
|
if !self.downloads.is_empty() {
|
||||||
self.list_state.select(Some(self.downloads.len() - 1));
|
self.list_state.select(Some(self.downloads.len() - 1));
|
||||||
}
|
}
|
||||||
@@ -198,7 +198,7 @@ impl MultiDownloadProgress {
|
|||||||
self.render_downloads_list(frame, chunks[1]);
|
self.render_downloads_list(frame, chunks[1]);
|
||||||
|
|
||||||
// Bottom: Controls
|
// Bottom: Controls
|
||||||
self.render_controls(frame, chunks[2]);
|
Self::render_controls(frame, chunks[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_stats(&self, frame: &mut Frame, area: ratatui::layout::Rect) {
|
fn render_stats(&self, frame: &mut Frame, area: ratatui::layout::Rect) {
|
||||||
@@ -312,7 +312,7 @@ impl MultiDownloadProgress {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_controls(&self, frame: &mut Frame, area: ratatui::layout::Rect) {
|
fn render_controls(frame: &mut Frame, area: ratatui::layout::Rect) {
|
||||||
let controls_line = format_shortcuts_styled(&[
|
let controls_line = format_shortcuts_styled(&[
|
||||||
("↑↓/jk", "scroll"),
|
("↑↓/jk", "scroll"),
|
||||||
("Home/End", "jump"),
|
("Home/End", "jump"),
|
||||||
@@ -402,7 +402,7 @@ pub fn render_download_stats(
|
|||||||
let stats_line = Line::from(vec![
|
let stats_line = Line::from(vec![
|
||||||
Span::raw("soulseek-rs 🦀 "),
|
Span::raw("soulseek-rs 🦀 "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("v{} ", VERSION),
|
format!("v{VERSION} "),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(COLOR_PRIMARY)
|
.fg(COLOR_PRIMARY)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
@@ -460,7 +460,7 @@ pub fn render_download_stats(
|
|||||||
spans.extend(data_str.spans);
|
spans.extend(data_str.spans);
|
||||||
spans.push(Span::raw(" • "));
|
spans.push(Span::raw(" • "));
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
format!("{}", speed_mb),
|
format!("{speed_mb}"),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(COLOR_PRIMARY)
|
.fg(COLOR_PRIMARY)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
@@ -487,9 +487,9 @@ pub fn show_multi_download_progress(
|
|||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
// Spawn background thread to initialize downloads
|
// Spawn background thread to initialize downloads
|
||||||
let init_client = client.clone();
|
let init_client = client;
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
for (filename, username, size) in selected_files.into_iter() {
|
for (filename, username, size) in selected_files {
|
||||||
// Initiate download
|
// Initiate download
|
||||||
match init_client.download(
|
match init_client.download(
|
||||||
filename.clone(),
|
filename.clone(),
|
||||||
@@ -501,10 +501,7 @@ pub fn show_multi_download_progress(
|
|||||||
let _ = tx.send((download, receiver));
|
let _ = tx.send((download, receiver));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
eprintln!("Failed to start download for {filename}: {e}");
|
||||||
"Failed to start download for {}: {}",
|
|
||||||
filename, e
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,13 +61,14 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
use ratatui::crossterm::{event::DisableMouseCapture, execute};
|
||||||
|
|
||||||
// Run the event loop, then restore the terminal unconditionally: if the
|
// Run the event loop, then restore the terminal unconditionally: if the
|
||||||
// loop returns early with an error the terminal must still be taken out
|
// loop returns early with an error the terminal must still be taken out
|
||||||
// of raw mode / the alternate screen and mouse capture disabled, or the
|
// of raw mode / the alternate screen and mouse capture disabled, or the
|
||||||
// user is left with a corrupted terminal.
|
// user is left with a corrupted terminal.
|
||||||
let result = self.run_event_loop(&mut terminal);
|
let result = self.run_event_loop(&mut terminal);
|
||||||
|
|
||||||
use ratatui::crossterm::{event::DisableMouseCapture, execute};
|
|
||||||
let _ = execute!(std::io::stdout(), DisableMouseCapture);
|
let _ = execute!(std::io::stdout(), DisableMouseCapture);
|
||||||
ratatui::restore();
|
ratatui::restore();
|
||||||
soulseek_rs::utils::logger::disable_buffering();
|
soulseek_rs::utils::logger::disable_buffering();
|
||||||
@@ -75,10 +76,7 @@ impl MainTui {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_event_loop(
|
fn run_event_loop(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
terminal: &mut DefaultTerminal,
|
|
||||||
) -> Result<()> {
|
|
||||||
while !self.state.should_exit {
|
while !self.state.should_exit {
|
||||||
terminal.draw(|frame| self.render(frame))?;
|
terminal.draw(|frame| self.render(frame))?;
|
||||||
|
|
||||||
@@ -95,10 +93,10 @@ impl MainTui {
|
|||||||
if poll(Duration::from_millis(100))? {
|
if poll(Duration::from_millis(100))? {
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
||||||
self.handle_key_event(key)?;
|
self.handle_key_event(key);
|
||||||
}
|
}
|
||||||
Event::Mouse(mouse) => {
|
Event::Mouse(mouse) => {
|
||||||
self.handle_mouse_event(mouse)?;
|
self.handle_mouse_event(mouse);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -328,7 +326,7 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
|
fn handle_key_event(&mut self, key: KeyEvent) {
|
||||||
// Command bar takes priority
|
// Command bar takes priority
|
||||||
if self.state.command_bar_active {
|
if self.state.command_bar_active {
|
||||||
return self.handle_command_bar_input(key);
|
return self.handle_command_bar_input(key);
|
||||||
@@ -345,25 +343,25 @@ impl MainTui {
|
|||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
self.state.should_exit = true;
|
self.state.should_exit = true;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
KeyCode::Char('1') => {
|
KeyCode::Char('1') => {
|
||||||
self.state.focused_pane = FocusedPane::Searches;
|
self.state.focused_pane = FocusedPane::Searches;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
KeyCode::Char('2') => {
|
KeyCode::Char('2') => {
|
||||||
self.state.focused_pane = FocusedPane::Results;
|
self.state.focused_pane = FocusedPane::Results;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
KeyCode::Char('3') => {
|
KeyCode::Char('3') => {
|
||||||
self.state.focused_pane = FocusedPane::Downloads;
|
self.state.focused_pane = FocusedPane::Downloads;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
KeyCode::Char('s') => {
|
KeyCode::Char('s') => {
|
||||||
self.state.command_bar_active = true;
|
self.state.command_bar_active = true;
|
||||||
self.state.command_bar_input.clear();
|
self.state.command_bar_input.clear();
|
||||||
self.state.command_bar_cursor_position = 0;
|
self.state.command_bar_cursor_position = 0;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -376,7 +374,7 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_command_bar_input(&mut self, key: KeyEvent) -> Result<()> {
|
fn handle_command_bar_input(&mut self, key: KeyEvent) {
|
||||||
self.state.command_bar_cursor_position = clamp_cursor_to_char_boundary(
|
self.state.command_bar_cursor_position = clamp_cursor_to_char_boundary(
|
||||||
&self.state.command_bar_input,
|
&self.state.command_bar_input,
|
||||||
self.state.command_bar_cursor_position,
|
self.state.command_bar_cursor_position,
|
||||||
@@ -474,10 +472,9 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_filter_input(&mut self, key: KeyEvent) -> Result<()> {
|
fn handle_filter_input(&mut self, key: KeyEvent) {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
self.state.results_is_filtering = false;
|
self.state.results_is_filtering = false;
|
||||||
@@ -497,10 +494,9 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_searches_input(&mut self, key: KeyEvent) -> Result<()> {
|
fn handle_searches_input(&mut self, key: KeyEvent) {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up | KeyCode::Char('k')
|
KeyCode::Up | KeyCode::Char('k')
|
||||||
if !self.state.searches.is_empty() =>
|
if !self.state.searches.is_empty() =>
|
||||||
@@ -551,10 +547,9 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_results_input(&mut self, key: KeyEvent) -> Result<()> {
|
fn handle_results_input(&mut self, key: KeyEvent) {
|
||||||
let items_count = if self.state.results_filter_query.is_empty() {
|
let items_count = if self.state.results_filter_query.is_empty() {
|
||||||
self.state.results_items.len()
|
self.state.results_items.len()
|
||||||
} else {
|
} else {
|
||||||
@@ -624,10 +619,9 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_downloads_input(&mut self, key: KeyEvent) -> Result<()> {
|
fn handle_downloads_input(&mut self, key: KeyEvent) {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up | KeyCode::Char('k')
|
KeyCode::Up | KeyCode::Char('k')
|
||||||
if !self.state.downloads.is_empty() =>
|
if !self.state.downloads.is_empty() =>
|
||||||
@@ -657,10 +651,9 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_selected_download_pause(&mut self) {
|
fn toggle_selected_download_pause(&self) {
|
||||||
let Some(index) = self.state.downloads_table_state.selected() else {
|
let Some(index) = self.state.downloads_table_state.selected() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -671,11 +664,13 @@ impl MainTui {
|
|||||||
let download = &download_entry.download;
|
let download = &download_entry.download;
|
||||||
match download.status {
|
match download.status {
|
||||||
DownloadStatus::InProgress { .. } => {
|
DownloadStatus::InProgress { .. } => {
|
||||||
self.client
|
let _ = self
|
||||||
|
.client
|
||||||
.pause_download(&download.username, &download.filename);
|
.pause_download(&download.username, &download.filename);
|
||||||
}
|
}
|
||||||
DownloadStatus::Paused { .. } => {
|
DownloadStatus::Paused { .. } => {
|
||||||
self.client
|
let _ = self
|
||||||
|
.client
|
||||||
.resume_download(&download.username, &download.filename);
|
.resume_download(&download.username, &download.filename);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -716,8 +711,8 @@ impl MainTui {
|
|||||||
self.state.downloads_table_state.select(Some(next_index));
|
self.state.downloads_table_state.select(Some(next_index));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_mouse_event(&mut self, mouse: MouseEvent) -> Result<()> {
|
fn handle_mouse_event(&mut self, mouse: MouseEvent) {
|
||||||
if let MouseEventKind::Down(MouseButton::Left) = mouse.kind {
|
if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
|
||||||
let (col, row) = (mouse.column, mouse.row);
|
let (col, row) = (mouse.column, mouse.row);
|
||||||
|
|
||||||
// Check if click is within searches pane
|
// Check if click is within searches pane
|
||||||
@@ -728,7 +723,7 @@ impl MainTui {
|
|||||||
&& row < area.y + area.height
|
&& row < area.y + area.height
|
||||||
{
|
{
|
||||||
self.state.focused_pane = FocusedPane::Searches;
|
self.state.focused_pane = FocusedPane::Searches;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if click is within results pane
|
// Check if click is within results pane
|
||||||
@@ -739,7 +734,7 @@ impl MainTui {
|
|||||||
&& row < area.y + area.height
|
&& row < area.y + area.height
|
||||||
{
|
{
|
||||||
self.state.focused_pane = FocusedPane::Results;
|
self.state.focused_pane = FocusedPane::Results;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if click is within downloads pane
|
// Check if click is within downloads pane
|
||||||
@@ -750,11 +745,8 @@ impl MainTui {
|
|||||||
&& row < area.y + area.height
|
&& row < area.y + area.height
|
||||||
{
|
{
|
||||||
self.state.focused_pane = FocusedPane::Downloads;
|
self.state.focused_pane = FocusedPane::Downloads;
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_search_at_index(&mut self, index: usize) {
|
fn remove_search_at_index(&mut self, index: usize) {
|
||||||
@@ -889,7 +881,7 @@ impl MainTui {
|
|||||||
// Results will be polled in update_search_results
|
// Results will be polled in update_search_results
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Search failed: {}", e);
|
eprintln!("Search failed: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -899,19 +891,14 @@ impl MainTui {
|
|||||||
let timeout = self.search_timeout;
|
let timeout = self.search_timeout;
|
||||||
let selected_search_index = self.state.selected_search_index;
|
let selected_search_index = self.state.selected_search_index;
|
||||||
|
|
||||||
// Collect all queries first
|
// Fetch all results in one go (single lock acquisition per query)
|
||||||
let queries: Vec<(usize, String)> = self
|
// Use try_get_search_results to avoid blocking the UI thread
|
||||||
|
let all_results: Vec<(usize, Vec<_>)> = self
|
||||||
.state
|
.state
|
||||||
.searches
|
.searches
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, s)| (idx, s.query.clone()))
|
.map(|(idx, s)| (idx, s.query.clone()))
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Fetch all results in one go (single lock acquisition per query)
|
|
||||||
// Use try_get_search_results to avoid blocking the UI thread
|
|
||||||
let all_results: Vec<(usize, Vec<_>)> = queries
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(idx, query)| {
|
.filter_map(|(idx, query)| {
|
||||||
self.client
|
self.client
|
||||||
.try_get_search_results(&query)
|
.try_get_search_results(&query)
|
||||||
@@ -989,7 +976,7 @@ impl MainTui {
|
|||||||
let sender = self.state.downloads_sender_channel.clone().unwrap();
|
let sender = self.state.downloads_sender_channel.clone().unwrap();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
for file in selected_files.into_iter() {
|
for file in selected_files {
|
||||||
let metadata = soulseek_rs::types::DownloadMetadata {
|
let metadata = soulseek_rs::types::DownloadMetadata {
|
||||||
bitrate: file.bitrate,
|
bitrate: file.bitrate,
|
||||||
length_seconds: file.length_seconds,
|
length_seconds: file.length_seconds,
|
||||||
@@ -1066,7 +1053,10 @@ pub fn launch_main_tui(
|
|||||||
tui.run(terminal)
|
tui.run(terminal)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clamp_cursor_to_char_boundary(input: &str, cursor_position: usize) -> usize {
|
const fn clamp_cursor_to_char_boundary(
|
||||||
|
input: &str,
|
||||||
|
cursor_position: usize,
|
||||||
|
) -> usize {
|
||||||
if cursor_position >= input.len() {
|
if cursor_position >= input.len() {
|
||||||
return input.len();
|
return input.len();
|
||||||
}
|
}
|
||||||
@@ -1084,8 +1074,7 @@ fn previous_char_boundary(input: &str, cursor_position: usize) -> usize {
|
|||||||
input[..cursor_position]
|
input[..cursor_position]
|
||||||
.char_indices()
|
.char_indices()
|
||||||
.last()
|
.last()
|
||||||
.map(|(index, _)| index)
|
.map_or(0, |(index, _)| index)
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_char_boundary(input: &str, cursor_position: usize) -> usize {
|
fn next_char_boundary(input: &str, cursor_position: usize) -> usize {
|
||||||
@@ -1099,8 +1088,7 @@ fn next_char_boundary(input: &str, cursor_position: usize) -> usize {
|
|||||||
+ input[cursor_position..]
|
+ input[cursor_position..]
|
||||||
.chars()
|
.chars()
|
||||||
.next()
|
.next()
|
||||||
.map(char::len_utf8)
|
.map_or(0, char::len_utf8)
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visible_input_at_cursor(
|
fn visible_input_at_cursor(
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ fn build_info_lines(
|
|||||||
lines.push(label_value("Save to", &save_path));
|
lines.push(label_value("Save to", &save_path));
|
||||||
|
|
||||||
if let Some(bitrate) = download.metadata.bitrate {
|
if let Some(bitrate) = download.metadata.bitrate {
|
||||||
lines.push(label_value("Bitrate", &format!("{} kbps", bitrate)));
|
lines.push(label_value("Bitrate", &format!("{bitrate} kbps")));
|
||||||
}
|
}
|
||||||
if let Some(length) = download.metadata.length_seconds {
|
if let Some(length) = download.metadata.length_seconds {
|
||||||
lines.push(label_value("Length", &format_duration(length)));
|
lines.push(label_value("Length", &format_duration(length)));
|
||||||
@@ -93,14 +93,14 @@ fn build_info_lines(
|
|||||||
DownloadStatus::Queued => {
|
DownloadStatus::Queued => {
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
let position_text = match download.queue_position {
|
let position_text = match download.queue_position {
|
||||||
Some(p) => format!("#{}", p),
|
Some(p) => format!("#{p}"),
|
||||||
None => "unknown".to_string(),
|
None => "unknown".to_string(),
|
||||||
};
|
};
|
||||||
lines.push(label_value("Queue position", &position_text));
|
lines.push(label_value("Queue position", &position_text));
|
||||||
|
|
||||||
if let Some(slots) = download.metadata.peer_free_slots {
|
if let Some(slots) = download.metadata.peer_free_slots {
|
||||||
let slot_text = if slots > 0 {
|
let slot_text = if slots > 0 {
|
||||||
format!("{} available", slots)
|
format!("{slots} available")
|
||||||
} else {
|
} else {
|
||||||
"all busy".to_string()
|
"all busy".to_string()
|
||||||
};
|
};
|
||||||
@@ -110,7 +110,7 @@ fn build_info_lines(
|
|||||||
if let Some(speed) = download.metadata.peer_upload_speed {
|
if let Some(speed) = download.metadata.peer_upload_speed {
|
||||||
lines.push(label_value(
|
lines.push(label_value(
|
||||||
"Peer upload speed",
|
"Peer upload speed",
|
||||||
&format_speed(speed as f64),
|
&format_speed(f64::from(speed)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,10 +149,7 @@ fn build_info_lines(
|
|||||||
|
|
||||||
fn label_value(label: &str, value: &str) -> Line<'static> {
|
fn label_value(label: &str, value: &str) -> Line<'static> {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(format!("{label:<LABEL_WIDTH$}"), dimmed_style()),
|
||||||
format!("{:<width$}", label, width = LABEL_WIDTH),
|
|
||||||
dimmed_style(),
|
|
||||||
),
|
|
||||||
Span::styled(value.to_string(), primary_style()),
|
Span::styled(value.to_string(), primary_style()),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -163,10 +160,7 @@ fn label_value_styled(
|
|||||||
value_style: Style,
|
value_style: Style,
|
||||||
) -> Line<'static> {
|
) -> Line<'static> {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(format!("{label:<LABEL_WIDTH$}"), dimmed_style()),
|
||||||
format!("{:<width$}", label, width = LABEL_WIDTH),
|
|
||||||
dimmed_style(),
|
|
||||||
),
|
|
||||||
Span::styled(value, value_style),
|
Span::styled(value, value_style),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -211,11 +205,11 @@ fn format_duration(seconds: u32) -> String {
|
|||||||
let minutes = (seconds % 3600) / 60;
|
let minutes = (seconds % 3600) / 60;
|
||||||
let secs = seconds % 60;
|
let secs = seconds % 60;
|
||||||
if hours > 0 {
|
if hours > 0 {
|
||||||
format!("{}h {:02}m {:02}s", hours, minutes, secs)
|
format!("{hours}h {minutes:02}m {secs:02}s")
|
||||||
} else if minutes > 0 {
|
} else if minutes > 0 {
|
||||||
format!("{}m {:02}s", minutes, secs)
|
format!("{minutes}m {secs:02}s")
|
||||||
} else {
|
} else {
|
||||||
format!("{}s", secs)
|
format!("{secs}s")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ pub fn render_results_pane(
|
|||||||
} = params;
|
} = params;
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
let title = if let Some(query) = active_search_query {
|
let title = if let Some(query) = active_search_query {
|
||||||
format!("[2] Results: {}", query)
|
format!("[2] Results: {query}")
|
||||||
} else {
|
} else {
|
||||||
"[2] Results".to_string()
|
"[2] Results".to_string()
|
||||||
};
|
};
|
||||||
@@ -75,13 +75,12 @@ pub fn render_results_pane(
|
|||||||
.title(title);
|
.title(title);
|
||||||
|
|
||||||
let message = if is_filtering {
|
let message = if is_filtering {
|
||||||
format!("No results match filter: '{}'", filter_query)
|
format!("No results match filter: '{filter_query}'")
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"soulseek-rs 🦀 v{}
|
"soulseek-rs 🦀 v{VERSION}
|
||||||
Michel de Graaf 2026\n
|
Michel de Graaf 2026\n
|
||||||
No results. Select a search from the Searches pane [1]. Or start new search [s → search] \n",
|
No results. Select a search from the Searches pane [1]. Or start new search [s → search] \n"
|
||||||
VERSION
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,13 +113,13 @@ No results. Select a search from the Searches pane [1]. Or start new search [s
|
|||||||
|
|
||||||
let bitrate_str = file
|
let bitrate_str = file
|
||||||
.bitrate
|
.bitrate
|
||||||
.map(|br| format!("{} kbps", br))
|
.map_or_else(|| "-".to_string(), |br| format!("{br} kbps"));
|
||||||
.unwrap_or_else(|| "-".to_string());
|
|
||||||
|
|
||||||
let speed_str = if file.speed > 0 {
|
let speed_str = if file.speed > 0 {
|
||||||
let speed_mb =
|
let speed_mb = (f64::from(file.speed) / BYTES_PER_MB * 100.0)
|
||||||
(file.speed as f64 / BYTES_PER_MB * 100.0).round() / 100.0;
|
.round()
|
||||||
format!("{} MB/s", speed_mb)
|
/ 100.0;
|
||||||
|
format!("{speed_mb} MB/s")
|
||||||
} else {
|
} else {
|
||||||
"-".to_string()
|
"-".to_string()
|
||||||
};
|
};
|
||||||
@@ -148,9 +147,9 @@ No results. Select a search from the Searches pane [1]. Or start new search [s
|
|||||||
];
|
];
|
||||||
|
|
||||||
let title = if is_filtering {
|
let title = if is_filtering {
|
||||||
format!("[2] Results - Filter: '{}'", filter_query)
|
format!("[2] Results - Filter: '{filter_query}'")
|
||||||
} else if let Some(query) = active_search_query {
|
} else if let Some(query) = active_search_query {
|
||||||
format!("[2] Results: {}", query)
|
format!("[2] Results: {query}")
|
||||||
} else {
|
} else {
|
||||||
"[2] Results".to_string()
|
"[2] Results".to_string()
|
||||||
};
|
};
|
||||||
@@ -178,7 +177,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn identity_mapping_used_when_no_filter() {
|
fn identity_mapping_used_when_no_filter() {
|
||||||
let selected: HashSet<usize> = [1].into_iter().collect();
|
let selected: HashSet<usize> = std::iter::once(1).collect();
|
||||||
assert!(row_is_selected(1, None, &selected));
|
assert!(row_is_selected(1, None, &selected));
|
||||||
assert!(!row_is_selected(0, None, &selected));
|
assert!(!row_is_selected(0, None, &selected));
|
||||||
}
|
}
|
||||||
@@ -188,7 +187,7 @@ mod tests {
|
|||||||
// Filter shows original rows 2, 5, 7 as display rows 0, 1, 2.
|
// Filter shows original rows 2, 5, 7 as display rows 0, 1, 2.
|
||||||
// The user selected original row 5.
|
// The user selected original row 5.
|
||||||
let original_indices = [2usize, 5, 7];
|
let original_indices = [2usize, 5, 7];
|
||||||
let selected: HashSet<usize> = [5].into_iter().collect();
|
let selected: HashSet<usize> = std::iter::once(5).collect();
|
||||||
assert!(!row_is_selected(0, Some(&original_indices), &selected));
|
assert!(!row_is_selected(0, Some(&original_indices), &selected));
|
||||||
assert!(row_is_selected(1, Some(&original_indices), &selected));
|
assert!(row_is_selected(1, Some(&original_indices), &selected));
|
||||||
assert!(!row_is_selected(2, Some(&original_indices), &selected));
|
assert!(!row_is_selected(2, Some(&original_indices), &selected));
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub fn render_searches_pane(
|
|||||||
{
|
{
|
||||||
"Searching...".to_string()
|
"Searching...".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{}", results_count)
|
format!("{results_count}")
|
||||||
};
|
};
|
||||||
|
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ pub fn unfocused_border_style() -> Style {
|
|||||||
Style::default()
|
Style::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focused_border_type() -> BorderType {
|
pub const fn focused_border_type() -> BorderType {
|
||||||
BorderType::Rounded
|
BorderType::Rounded
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfocused_border_type() -> BorderType {
|
pub const fn unfocused_border_type() -> BorderType {
|
||||||
BorderType::Rounded
|
BorderType::Rounded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ pub fn border_style(focused: bool) -> Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn border_type(focused: bool) -> BorderType {
|
pub const fn border_type(focused: bool) -> BorderType {
|
||||||
if focused {
|
if focused {
|
||||||
focused_border_type()
|
focused_border_type()
|
||||||
} else {
|
} else {
|
||||||
@@ -100,9 +100,7 @@ pub fn border_type(focused: bool) -> BorderType {
|
|||||||
pub fn format_shortcuts(shortcuts: &[(&str, &str)]) -> String {
|
pub fn format_shortcuts(shortcuts: &[(&str, &str)]) -> String {
|
||||||
shortcuts
|
shortcuts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, action)| {
|
.map(|(key, action)| format!("[{key} {HIGHLIGHT_SYMBOL} {action}]"))
|
||||||
format!("[{} {} {}]", key, HIGHLIGHT_SYMBOL, action)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" ")
|
.join(" ")
|
||||||
}
|
}
|
||||||
@@ -124,7 +122,7 @@ pub fn format_shortcuts_styled(shortcuts: &[(&str, &str)]) -> Line<'static> {
|
|||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
format!(" {} ", HIGHLIGHT_SYMBOL),
|
format!(" {HIGHLIGHT_SYMBOL} "),
|
||||||
dimmed_style(),
|
dimmed_style(),
|
||||||
));
|
));
|
||||||
spans.push(Span::raw(action.to_string()));
|
spans.push(Span::raw(action.to_string()));
|
||||||
@@ -165,7 +163,7 @@ pub fn format_progress_bar(
|
|||||||
Span::raw("]"),
|
Span::raw("]"),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}%", percentage),
|
format!("{percentage}%"),
|
||||||
primary_style()
|
primary_style()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.fg(Color::default()),
|
.fg(Color::default()),
|
||||||
|
|||||||
@@ -39,6 +39,6 @@ pub fn get_bitrate(
|
|||||||
const SPINNER_CHARS: [&str; 10] =
|
const SPINNER_CHARS: [&str; 10] =
|
||||||
["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
|
|
||||||
pub fn get_spinner_char(state: usize) -> &'static str {
|
pub const fn get_spinner_char(state: usize) -> &'static str {
|
||||||
SPINNER_CHARS[state % SPINNER_CHARS.len()]
|
SPINNER_CHARS[state % SPINNER_CHARS.len()]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user