From 8a6b74af63b79caf961c60c02e66f6f006922cce Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 11 May 2026 17:27:47 +0100 Subject: [PATCH 1/2] feat(tracker-client): add http response format option --- .../src/console/clients/http/app.rs | 108 ++++++++++++++++-- 1 file changed, 96 insertions(+), 12 deletions(-) diff --git a/console/tracker-client/src/console/clients/http/app.rs b/console/tracker-client/src/console/clients/http/app.rs index e412c7245..1bce90329 100644 --- a/console/tracker-client/src/console/clients/http/app.rs +++ b/console/tracker-client/src/console/clients/http/app.rs @@ -6,7 +6,15 @@ //! `Announce` request: //! //! ```text -//! cargo run --bin http_tracker_client announce http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! cargo run --bin http_tracker_client announce http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 +//! ``` +//! +//! `Announce` request (pretty JSON output): +//! +//! ```text +//! cargo run --bin http_tracker_client announce \ +//! http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 \ +//! --format pretty //! ``` //! //! `Announce` request (all optional parameters): @@ -27,7 +35,15 @@ //! `Scrape` request: //! //! ```text -//! cargo run --bin http_tracker_client scrape http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! cargo run --bin http_tracker_client scrape http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 +//! ``` +//! +//! `Scrape` request (pretty JSON output): +//! +//! ```text +//! cargo run --bin http_tracker_client scrape \ +//! http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 \ +//! --format pretty //! ``` //! //! Unrecognized response fallback (generic JSON): @@ -91,6 +107,12 @@ impl From for Compact { } } +#[derive(Clone, Copy, Debug, ValueEnum)] +enum OutputFormat { + Compact, + Pretty, +} + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { @@ -119,10 +141,14 @@ enum Command { peer_id: Option, #[arg(long, value_enum)] compact: Option, + #[arg(long, value_enum, default_value_t = OutputFormat::Compact)] + format: OutputFormat, }, Scrape { tracker_url: String, info_hashes: Vec, + #[arg(long, value_enum, default_value_t = OutputFormat::Compact)] + format: OutputFormat, }, } @@ -137,6 +163,7 @@ struct AnnounceOptions { peer_addr: Option, peer_id: Option, compact: Option, + output_format: OutputFormat, } /// # Errors @@ -157,6 +184,7 @@ pub async fn run() -> anyhow::Result<()> { peer_addr, peer_id, compact, + format, } => { announce_command( AnnounceOptions { @@ -170,6 +198,7 @@ pub async fn run() -> anyhow::Result<()> { peer_addr, peer_id, compact, + output_format: format, }, DEFAULT_TIMEOUT, ) @@ -178,8 +207,9 @@ pub async fn run() -> anyhow::Result<()> { Command::Scrape { tracker_url, info_hashes, + format, } => { - scrape_command(&tracker_url, &info_hashes, DEFAULT_TIMEOUT).await?; + scrape_command(&tracker_url, &info_hashes, format, DEFAULT_TIMEOUT).await?; } } @@ -227,11 +257,13 @@ async fn announce_command(options: AnnounceOptions, timeout: Duration) -> anyhow let body = response.bytes().await?; let json = if let Ok(announce_response) = serde_bencode::from_bytes::(&body) { - serde_json::to_string(&announce_response).context("failed to serialize announce response into JSON")? + serialize_json(&announce_response, options.output_format).context("failed to serialize announce response into JSON")? } else if let Ok(compact_response) = serde_bencode::from_bytes::(&body) { - serde_json::to_string(&compact_response).context("failed to serialize compact announce response into JSON")? + serialize_json(&compact_response, options.output_format) + .context("failed to serialize compact announce response into JSON")? } else { - let fallback = bencode_to_fallback_json_or_raw_bytes(&body); + let fallback = bencode_to_fallback_json_or_raw_bytes(&body, options.output_format) + .context("failed to serialize fallback announce response into JSON")?; println!("{fallback}"); @@ -268,7 +300,12 @@ fn parse_non_zero_port(port_str: &str) -> anyhow::Result { Ok(port) } -async fn scrape_command(tracker_url: &str, info_hashes: &[String], timeout: Duration) -> anyhow::Result<()> { +async fn scrape_command( + tracker_url: &str, + info_hashes: &[String], + output_format: OutputFormat, + timeout: Duration, +) -> anyhow::Result<()> { let base_url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?; let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?; @@ -278,23 +315,70 @@ async fn scrape_command(tracker_url: &str, info_hashes: &[String], timeout: Dura let body = response.bytes().await?; let Ok(scrape_response) = scrape::Response::try_from_bencoded(&body) else { - let fallback = bencode_to_fallback_json_or_raw_bytes(&body); + let fallback = bencode_to_fallback_json_or_raw_bytes(&body, output_format) + .context("failed to serialize fallback scrape response into JSON")?; println!("{fallback}"); bail!("unrecognized scrape response from tracker") }; - let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?; + let json = serialize_json(&scrape_response, output_format).context("failed to serialize scrape response into JSON")?; println!("{json}"); Ok(()) } -fn bencode_to_fallback_json_or_raw_bytes(body: &[u8]) -> String { +fn bencode_to_fallback_json_or_raw_bytes(body: &[u8], output_format: OutputFormat) -> anyhow::Result { match try_bencode_to_json(body) { - Ok(json) => json, - Err(_) => format!("Warning: Could not deserialize HTTP tracker response. Raw bytes: {body:?}"), + Ok(json) => { + let value: serde_json::Value = serde_json::from_str(&json).context("failed to parse fallback bencode JSON")?; + + serialize_json(&value, output_format).context("failed to format fallback bencode JSON") + } + Err(_) => Ok(format!( + "Warning: Could not deserialize HTTP tracker response. Raw bytes: {body:?}" + )), + } +} + +fn serialize_json(value: &T, output_format: OutputFormat) -> anyhow::Result { + match output_format { + OutputFormat::Compact => serde_json::to_string(value).context("failed to serialize JSON"), + OutputFormat::Pretty => serde_json::to_string_pretty(value).context("failed to serialize pretty JSON"), + } +} + +#[cfg(test)] +mod tests { + use serde::Serialize; + + use super::{serialize_json, OutputFormat}; + + #[derive(Serialize)] + struct Sample { + seeders: i32, + leechers: i32, + } + + #[test] + fn it_should_serialize_compact_json() { + let data = Sample { seeders: 1, leechers: 2 }; + + let json = serialize_json(&data, OutputFormat::Compact).expect("it should serialize compact JSON"); + + assert_eq!(json, "{\"seeders\":1,\"leechers\":2}"); + } + + #[test] + fn it_should_serialize_pretty_json() { + let data = Sample { seeders: 1, leechers: 2 }; + + let json = serialize_json(&data, OutputFormat::Pretty).expect("it should serialize pretty JSON"); + + assert!(json.contains('\n')); + assert!(json.contains(" \"seeders\": 1")); + assert!(json.contains(" \"leechers\": 2")); } } From 2bfeabd262c295b88b4fc04d4241b8bd432d1a39 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 11 May 2026 18:38:47 +0100 Subject: [PATCH 2/2] refactor(tracker-client): optimize compact fallback formatting --- .../tracker-client/src/console/clients/http/app.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/console/tracker-client/src/console/clients/http/app.rs b/console/tracker-client/src/console/clients/http/app.rs index 1bce90329..f146c5001 100644 --- a/console/tracker-client/src/console/clients/http/app.rs +++ b/console/tracker-client/src/console/clients/http/app.rs @@ -332,11 +332,14 @@ async fn scrape_command( fn bencode_to_fallback_json_or_raw_bytes(body: &[u8], output_format: OutputFormat) -> anyhow::Result { match try_bencode_to_json(body) { - Ok(json) => { - let value: serde_json::Value = serde_json::from_str(&json).context("failed to parse fallback bencode JSON")?; - - serialize_json(&value, output_format).context("failed to format fallback bencode JSON") - } + Ok(json) => match output_format { + OutputFormat::Compact => Ok(json), + OutputFormat::Pretty => { + let value: serde_json::Value = serde_json::from_str(&json).context("failed to parse fallback bencode JSON")?; + + serialize_json(&value, output_format).context("failed to format fallback bencode JSON") + } + }, Err(_) => Ok(format!( "Warning: Could not deserialize HTTP tracker response. Raw bytes: {body:?}" )),