Commit Graph
136 Commits
Author SHA1 Message Date
michel f61e02e05c test: broaden soulfind e2e coverage of implemented features
Cover implemented, server-verifiable paths that were previously untested:

- assert an issued search is registered in client state, not just that
  results are empty;
- a_search_is_forwarded_to_a_connected_peer: a second client exercises the
  incoming FileSearch handler (the server distributes searches to it), and we
  prove that session survives by round-tripping its own search afterwards;
- login_succeeds_with_listener_enabled: drives the post-login handshake with
  the peer listener on (SetWaitPort), which every other e2e test skips.

Peer downloads / file sharing remain out of scope here: they need a second
peer that actually serves files, which the client does not implement yet.
2026-07-19 13:27:30 +02:00
michel a127dd97bf ci: run soulfind e2e tests on GitHub Actions
Extend the existing CI workflow with an ubuntu-only "End-to-end (soulfind)"
job that builds soulfind from source (LDC + `dub build :server`), points
SOULFIND_BIN at it, and runs the e2e suite with SOULSEEK_E2E_REQUIRED=1.
Also widen the clippy job to the whole workspace.

The e2e harness gains SOULSEEK_E2E_REQUIRED: when set, a missing/unstartable
server is a hard failure instead of a silent skip, so CI genuinely exercises
the client end-to-end. Add e2e coverage for two existing behaviours a single
server can verify: wrong-password rejection and concurrent logins.
2026-07-19 13:19:46 +02:00
michel 1328736550 feat: configure download & upload (shared) directories
Add a --shared-dir / SOULSEEK_SHARED_DIR option and a SOULSEEK_DOWNLOAD_DIR
env for the download directory. The shared directory is resolved and
validated up front (tilde-expanded, must exist and be a directory); a
misconfigured value is a warning, not a fatal error.

The library gains an optional shared_directory on ClientSettings/Client
with a getter, keeping soulseek-rs-lib dependency-free. Resolution lives
in a new soulseek-rs/src/directories.rs with unit tests.
2026-07-19 13:12:44 +02:00
michel 9035ecebcb test: add server-optional e2e integration harness against soulfind
Adds soulseek-rs-lib/tests/e2e.rs, which drives the client's public API
(connect, login, search) against a real Soulseek server. The harness is
server-optional so cargo test stays green everywhere: it uses
SOULSEEK_TEST_SERVER if set, otherwise spawns a soulfind binary (SOULFIND_BIN
or a soulfind/bin/soulfind checkout in a parent dir) on an ephemeral port with
a throwaway database, and skips with a notice when no server is available.

Uses only std and the library's public API — no new dependencies. Documents how
to run it (including the macOS soulfind SQLite workaround) in the README.
2026-07-19 00:13:35 +02:00
michel e72c8a5c07 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.
2026-07-19 00:09:17 +02:00
michel b149a8ecf2 fix: always restore the terminal when the TUI loop errors
MainTui::run only disabled mouse capture and called ratatui::restore() after
the event loop exited normally; any error propagated by ? from draw/poll/read
returned early and left the terminal in raw mode, on the alternate screen, with
mouse capture still enabled. The loop body moved into a helper whose result is
returned only after the terminal is unconditionally restored.
2026-07-18 14:37:35 +02:00
michel 15ac923bd9 fix: stop the previous peer actor when re-registering a username
register_peer inserted the new handle and silently dropped the old one, leaving
the previous actor running as an orphan that pins a thread-pool worker forever;
its later shutdown could also remove the replacement connection from the
registry. The displaced actor is now told to stop.
2026-07-18 14:37:35 +02:00
michel c5f57cccea fix: resolve self-deadlock in PeerDisconnected handler
The handler held a read guard on the client-context RwLock while calling
process_failed_uploads, which acquires a write guard on the same lock on the
same thread. std RwLock write() then blocks forever, wedging the entire client
operations loop. The read guard is now scoped to just the registry cleanup and
dropped before the write is taken.
2026-07-18 14:37:35 +02:00
michel 846d9750b4 fix: stop duplicating every log line to the file
When log buffering and a LOG_FILE were both active, log() wrote each line to
the file immediately and also pushed it to the buffer, which flush_buffered_logs
then wrote to the file a second time. Each line now routes to exactly one sink
(file / buffer / stderr) via a small tested decision function; a configured
file always bypasses buffering, so nothing is written twice.
2026-07-18 14:33:39 +02:00
michel ce244e4e91 fix: contain panicking jobs so a thread-pool worker is never lost
A job that panicked unwound out of the worker's run loop and permanently
killed that worker; the pool has a fixed worker count and never respawns, so
repeated panics (e.g. from malformed network messages) would exhaust the pool
and wedge the whole actor system — a DoS. The worker now runs each job under
catch_unwind (the shared receiver lock is already released, so a panic cannot
poison it) and survives to process the next job. Drop and execute no longer
unwrap their channel/join results, so shutdown can never itself panic.
2026-07-18 14:33:39 +02:00
michel bdaeb33a4f fix: TUI selection underflow and filtered-results desync
Three related TUI correctness bugs:

- download_selector navigation (select_previous/next/last) computed
  items.len() - 1, panicking with an arithmetic underflow when the list was
  empty (e.g. a filter matched nothing). Index math is now in wrap-safe
  helpers that return no selection for an empty list.

- update_search_results overwrote the filtered results view with the full
  unfiltered list on every ~100ms poll, so an active filter silently stopped
  working as new results streamed in. It now re-derives the filtered view
  from the current query.

- The results pane rendered selection checkboxes by testing the display-row
  index against a selection set that stores unfiltered indices, so under an
  active filter the wrong rows showed as checked. It now maps the display
  index back through the filtered-index table.

Filtering and index-mapping logic extracted into pure, unit-tested helpers.
2026-07-18 14:30:02 +02:00
michel 16af88eb9d fix: error on degenerate Huffman code in zlib inflate instead of panicking
decode_symbol walked the Huffman tree while either child existed, then
unwrapped the child selected by the current bit. A malformed/incomplete code
table (fully attacker-controlled, since search results arrive zlib-compressed
from peers) can leave a node with only one child; reading the opposite bit
unwrapped None and panicked, unwinding out of the actor job. It now returns a
decode error, which propagates as a CompressionError and is handled gracefully.
2026-07-18 14:23:40 +02:00
michel 96988f9f9e fix: detect truncated downloads instead of saving them as Completed
read_download_stream broke out of its read loop on peer EOF (Ok(0)) without
checking whether the promised number of bytes had actually arrived, then saved
the partial buffer and the caller reported DownloadStatus::Completed. A peer
that aborts mid-transfer thus produced a truncated file marked complete. The
buffer was also never trimmed, so trailing coalesced bytes were written past
the file's real size.

finalize_download_buffer now errors with IncompleteDownload when fewer bytes
than expected arrived (caller maps this to Failed) and trims any overshoot to
the exact expected size. The listener's incoming-file path now also marks the
download Failed on error rather than only logging, so it can no longer stay
stuck Queued/InProgress forever.
2026-07-18 14:22:32 +02:00
michel 00e4036ac2 fix: reject malformed peer connection types instead of panicking
Peer::new_from_message, NewPeer::new_from_message and the listener's
parse_peer_init_message parsed an untrusted connection-type string with
.parse().unwrap(), panicking on anything other than P/F/D. Reachable from:
- a malicious server via ConnectToPeer (kills a thread-pool worker), and
- any peer connecting to our listener (kills the accept loop).

new_from_message now returns Option and callers drop malformed messages;
the listener rejects the connection. Added a regression test for truncated
TransferRequest parsing, now safe via the bounds-checked readers.
2026-07-18 14:19:50 +02:00
michel 689d8b5de1 fix: harden Message reader against malformed network input
read_string, read_int8, read_int64, read_bool and the get_message_code
accessors indexed/sliced the buffer using network-controlled offsets and
lengths with no bounds checks. An untrusted server or peer could send a
message with an oversized string length or a truncated/zero-length frame and
panic the parser; the panic unwinds out of the actor job and permanently
kills a thread-pool worker (DoS + worker leak).

All readers now bounds-check and return a safe default (empty/0/false) on
truncation, matching the existing convention already used by read_int32 and
read_raw_byte.
2026-07-18 14:17:06 +02:00
michel daac398f68 chore: update Rust toolchain and dependencies to latest
- clap 4.5.51 -> 4.6.2
- ratatui 0.29.0 -> 0.30.2
- cargo update for transitive deps
- toolchains updated: stable 1.97.1, nightly 1.99.0
2026-07-18 12:24:03 +02:00
michel d06aa2d3de style: apply cargo fmt 2026-04-26 17:28:43 +02:00
michel 9d6a33da34 refactor: centralize BYTES_PER_MB and tidy peer IP parsing
- Add pub const BYTES_PER_MB to ui/utils.rs and replace 7 uses of the
  bare 1_048_576.0 literal across the UI modules. Also pick up
  uninlined-format-args cleanup in the same file while we're here.
- Replace Vec<i32> IP-byte accumulator in Peer::new_from_message with a
  [u8; 4]; the .abs() calls on u8.into() were no-ops on always-positive
  values. Network byte order ([3].[2].[1].[0]) preserved; tests cover
  the wire format.
2026-04-26 17:20:45 +02:00
michel e0218b2433 fix: use u16 for listen port end-to-end
Port numbers are 16-bit; the codebase had listen_port/listener_port
typed as u32, inconsistent with PeerAddress.port (already u16).
Migrate the type all the way through the CLI, config, ClientSettings,
Client, ServerActor, and the listener entry point. The wire format is
preserved: build_set_wait_port_message now takes u16 and widens to u32
via .into() before write_int32. Also rename DEFALT_LISTEN_PORT to
DEFAULT_LISTEN_PORT.
2026-04-26 17:20:45 +02:00
michel 44846cdfa8 chore: remove dead code and stale comments
- Drop dead Rooms/Room infrastructure and the rooms field on Context.
- Remove unused ActorMessage::Tick variant and the now-empty handler arm.
- Strip dead active-thread tracking machinery from ThreadPool (unused
  getters, _id/_active_threads/_total_threads parameters).
- Flatten ServerActor::initiate_connection: the duplicated retry block
  was a copy-paste retry against the same address with the same args
  and could never succeed where the first attempt failed.
- Delete commented-out print_hex2/decode debug stubs, dead test
  fixtures in login.rs and message_user.rs, the 47-line commented body
  of RoomListHandler, and assorted // TODO theater.
2026-04-26 17:20:45 +02:00
michel 574833bf61 feat: add download info pane to TUI
Splits the downloads area 60/40 with a new info pane that adapts to the
selected download's status: header (filename, user, size, status, save
path, bitrate, length) plus queued-specific fields (queue position, peer
slots, peer upload speed) or live progress/speed/ETA when transferring.

Plumbs queue position via a new ClientOperation::PlaceInQueueUpdate so
PlaceInQueueResponse messages from peers reach DownloadStore, and threads
search-time metadata (bitrate, length, peer slots/speed) onto Download
through a new download_with_metadata entry point.
2026-04-26 17:19:26 +02:00
michel fa962a0dea Merge branch 'refactor/extract-download-state'
# Conflicts:
#	soulseek-rs-lib/src/client.rs
2026-04-26 12:52:50 +02:00
michel 4775ede762 Merge branch 'refactor/lock-poison-safety' 2026-04-26 12:51:49 +02:00
michel 28baebdb2e refactor: extract DownloadStore from ClientContext
Move the Vec<Download> and the download lifecycle methods (add, remove,
lookup, status update, pause, resume, remove-if-queued) into a new
download_store module. ClientContext keeps thin delegating shims so
peer/listen, peer/download_peer and the existing client tests carry on
unchanged. Client::pause_download / resume_download / remove_queued_download
now delegate one-liners. Client::process_failed_uploads keeps the
read-then-write lock dance but the inner filtering is the new
collect_failed_tokens helper.

Adds dedicated unit tests for the store's behavior (add/get/remove,
pause+resume, queued-only removal, failed-token collection) so the
state-machine logic is exercised independently of Client.
2026-04-26 12:36:30 +02:00
michel 2722552e35 refactor: replace lock unwraps with poison-aware helpers
Add SoulseekRs::LockPoisoned variant and a small RwLockExt/MutexExt
helper module exposing read_safe/write_safe/lock_safe that map
PoisonError to that variant. Walk every non-test lock acquisition
across client, peer/server actors, peer registry, listener and the
thread pool: propagate via ? where the function returns Result, log
and degrade to a sensible default (return early, false, None, empty)
where it does not. Client::connect now returns Result so the CLI can
surface the failure instead of panicking.
2026-04-26 12:32:46 +02:00
michel e1ee3ac493 fix clippy collapsible match warnings 2026-04-26 12:18:00 +02:00
michel 41d129c6bc chore: update license date 2026-04-26 12:00:24 +02:00
Michel de Graaf 6a63703f82 Merge pull request #2 from adalessandro/wip/adalessandro/dotenv
soulseek-rs: main: Add support for dotenv environment
2026-04-26 11:58:29 +02:00
Michel de Graaf dfdd60f1c8 Merge pull request #1 from adalessandro/dev
soulseek-rs: main_tui: Restore terminal status before exiting
2026-04-26 11:57:54 +02:00
michel bff7ca4bd4 chore: change copyright date 2026-04-26 11:55:40 +02:00
michel de58c8cbe2 Add TUI download pause and queued delete 2026-04-26 11:50:37 +02:00
michel 807dea5f17 Fix TUI search cursor editing 2026-04-26 11:43:44 +02:00
Ariel D'Alessandro 3696e95760 soulseek-rs: main: Add support for dotenv environment
Signed-off-by: Ariel D'Alessandro <ariel.dalessandro@collabora.com>
2026-01-29 19:36:15 -03:00
Ariel D'Alessandro 9627fa475d soulseek-rs: main_tui: Restore terminal status before exiting
Signed-off-by: Ariel D'Alessandro <ariel.dalessandro@collabora.com>
2026-01-29 17:16:19 -03:00
Michel de Graaf dfe6f532fa chore: formatting 2026-01-04 20:37:03 +01:00
Michel de Graaf 94b33d4bd6 chore: get rid of generated files 2026-01-04 20:12:51 +01:00
mayor 16c8a4bde9 chore: update to Rust 2024 edition 2026-01-04 20:08:16 +01:00
Michel de Graaf f7ebcccdd3 chore: ignore beads 2026-01-04 19:50:44 +01:00
Michel de Graaf f5b65053e2 fix: hanging TUI on queueing downloads 2025-11-19 09:13:03 +01:00
Michel de Graaf 9f174c4c5e fix: linting 2025-11-19 08:40:41 +01:00
Michel de Graaf bb86e50b4f fix: queuing downloads 2025-11-19 08:27:50 +01:00
Michel de Graaf b7a5037edb chore: keep updating search results 2025-11-18 16:02:19 +01:00
Michel de Graaf 2ba48663e4 feat: logging to log file 2025-11-18 15:49:53 +01:00
Michel de Graaf 4c6d7c6850 chore: refactor UI to use Downloads struct 2025-11-18 14:33:33 +01:00
Michel de Graaf 79da19ed0f chore: refactor downloads 2025-11-18 14:11:25 +01:00
Michel de Graaf 467de9cb91 chore: bump version 2025-11-13 14:13:28 +01:00
Michel de Graaf 9a4ad6c38a feat: main TUI 2025-11-13 14:12:37 +01:00
Michel de Graaf cafa40a09d chore: refactor to support multi search 2025-11-13 10:28:16 +01:00
Michel de Graaf 4a34583f9c chore: improve download selector 2025-11-13 10:08:16 +01:00
Michel de Graaf 6ca179373f chore: bump version 2025-11-12 21:18:36 +01:00