Skip to content

Commit d9011be

Browse files
authored
Merge pull request #155 from jwodder/gh-127
Give `query-tracker` a `--json` option
2 parents dc5258c + fc07fd4 commit d9011be

File tree

6 files changed

+229
-4
lines changed

6 files changed

+229
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ v0.4.0 (in development)
33
- Increased MSRV to 1.81
44
- Linux release artifacts are now built on Ubuntu 22.04 (up from Ubuntu 20.04),
55
which may result in a more recent glibc being required
6+
- Added a `--json` option to `query-tracker`
67

78
v0.3.1 (2025-02-23)
89
-------------------

Cargo.lock

Lines changed: 87 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ thiserror = "2.0.0"
4040

4141
[dev-dependencies]
4242
assert_cmd = "2.0.14"
43+
rstest = { version = "0.25.0", default-features = false }
4344
tempfile = "3.10.1"
4445

4546
[features]

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,17 @@ the `--outfile` option.
145145
`demagnetize query-tracker`
146146
---------------------------
147147

148-
demagnetize [<global options>] query-tracker <tracker> <info-hash>
148+
demagnetize [<global options>] query-tracker [<options>] <tracker> <info-hash>
149149

150150
Query the given tracker (specified as an HTTP or UDP URL) for peers serving the
151151
torrent with the given info hash (specified as a 40-character hex string or
152152
32-character base32 string), and print out the the retrieved peers' addresses
153153
in the form "IP:PORT".
154154

155+
### Options
156+
157+
- `-J`, `--json` — Print out the peers as JSON objects, one per line
158+
155159

156160
`demagnetize query-peer`
157161
------------------------

src/main.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ enum Command {
7979
},
8080
/// Fetch peers for an info hash from a specific tracker
8181
QueryTracker {
82+
/// Output peers as JSON objects, one per line
83+
#[arg(short = 'J', long)]
84+
json: bool,
85+
8286
/// The tracker to scrape, as an HTTP or UDP URL.
8387
tracker: Tracker,
8488

@@ -182,15 +186,23 @@ impl Command {
182186
ExitCode::FAILURE
183187
}
184188
}
185-
Command::QueryTracker { tracker, info_hash } => {
189+
Command::QueryTracker {
190+
json,
191+
tracker,
192+
info_hash,
193+
} => {
186194
let group = Arc::new(ShutdownGroup::new());
187195
let r = match tracker
188196
.get_peers(info_hash, local, Arc::clone(&group))
189197
.await
190198
{
191199
Ok(peers) => {
192200
for p in peers {
193-
println!("{}", p.address);
201+
if json {
202+
println!("{}", p.display_json());
203+
} else {
204+
println!("{}", p.address);
205+
}
194206
}
195207
ExitCode::SUCCESS
196208
}

src/peer.rs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::types::{InfoHash, LocalPeer, PeerId};
1010
use bendy::decoding::{Error as BendyError, FromBencode, Object, ResultExt};
1111
use bytes::{Bytes, BytesMut};
1212
use futures_util::{SinkExt, StreamExt};
13-
use std::fmt;
13+
use std::fmt::{self, Write};
1414
use std::net::{AddrParseError, IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6};
1515
use std::str::FromStr;
1616
use thiserror::Error;
@@ -111,6 +111,10 @@ impl Peer {
111111
metadata_size,
112112
})
113113
}
114+
115+
pub(crate) fn display_json(&self) -> DisplayJson<'_> {
116+
DisplayJson(self)
117+
}
114118
}
115119

116120
impl FromStr for Peer {
@@ -371,10 +375,61 @@ pub(crate) enum PeerError {
371375
Unexpected(String),
372376
}
373377

378+
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
379+
pub(crate) struct DisplayJson<'a>(&'a Peer);
380+
381+
impl fmt::Display for DisplayJson<'_> {
382+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383+
write!(
384+
f,
385+
r#"{{"host": "{}", "port": {}, "id": "#,
386+
self.0.address.ip(),
387+
self.0.address.port()
388+
)?;
389+
if let Some(ref pid) = self.0.id {
390+
f.write_char('"')?;
391+
for chunk in pid.as_bytes().utf8_chunks() {
392+
write_json_str(chunk.valid(), f)?;
393+
if !chunk.invalid().is_empty() {
394+
write!(f, "\\ufffd")?;
395+
}
396+
}
397+
f.write_char('"')?;
398+
} else {
399+
write!(f, "null")?;
400+
}
401+
f.write_char('}')?;
402+
Ok(())
403+
}
404+
}
405+
406+
fn write_json_str<W: Write>(s: &str, writer: &mut W) -> fmt::Result {
407+
for c in s.chars() {
408+
match c {
409+
'"' => writer.write_str("\\\"")?,
410+
'\\' => writer.write_str(r"\\")?,
411+
'\x08' => writer.write_str("\\b")?,
412+
'\x0C' => writer.write_str("\\f")?,
413+
'\n' => writer.write_str("\\n")?,
414+
'\r' => writer.write_str("\\r")?,
415+
'\t' => writer.write_str("\\t")?,
416+
' '..='~' => writer.write_char(c)?,
417+
c => {
418+
let mut buf = [0u16; 2];
419+
for b in c.encode_utf16(&mut buf) {
420+
write!(writer, "\\u{b:04x}")?;
421+
}
422+
}
423+
}
424+
}
425+
Ok(())
426+
}
427+
374428
#[cfg(test)]
375429
mod tests {
376430
use super::*;
377431
use crate::util::{decode_bencode, UnbencodeError};
432+
use rstest::rstest;
378433

379434
#[test]
380435
fn test_unbencode_peer() {
@@ -435,4 +490,69 @@ mod tests {
435490
);
436491
assert!(matches!(r, Err(UnbencodeError::TrailingData)));
437492
}
493+
494+
mod display_json {
495+
use super::*;
496+
497+
#[test]
498+
fn no_id() {
499+
let peer = "127.0.0.1:8080".parse::<Peer>().unwrap();
500+
let s = peer.display_json().to_string();
501+
assert_eq!(s, r#"{"host": "127.0.0.1", "port": 8080, "id": null}"#);
502+
}
503+
504+
#[test]
505+
fn simple_id() {
506+
let peer = decode_bencode::<Peer>(
507+
b"d2:ip9:127.0.0.17:peer id20:-PRE-123-abcdefghijk4:porti8080ee",
508+
)
509+
.unwrap();
510+
let s = peer.display_json().to_string();
511+
assert_eq!(
512+
s,
513+
r#"{"host": "127.0.0.1", "port": 8080, "id": "-PRE-123-abcdefghijk"}"#
514+
);
515+
}
516+
517+
#[test]
518+
fn non_ascii_id() {
519+
let peer = decode_bencode::<Peer>(
520+
b"d2:ip9:127.0.0.17:peer id20:-PRE-123-abcdefgh\xC3\xAEj4:porti8080ee",
521+
)
522+
.unwrap();
523+
let s = peer.display_json().to_string();
524+
assert_eq!(
525+
s,
526+
r#"{"host": "127.0.0.1", "port": 8080, "id": "-PRE-123-abcdefgh\u00eej"}"#
527+
);
528+
}
529+
530+
#[test]
531+
fn non_utf8_id() {
532+
let peer = decode_bencode::<Peer>(
533+
b"d2:ip9:127.0.0.17:peer id20:-PRE-123-abcdefgh\xEEjk4:porti8080ee",
534+
)
535+
.unwrap();
536+
let s = peer.display_json().to_string();
537+
assert_eq!(
538+
s,
539+
r#"{"host": "127.0.0.1", "port": 8080, "id": "-PRE-123-abcdefgh\ufffdjk"}"#
540+
);
541+
}
542+
}
543+
544+
#[rstest]
545+
#[case("foobar", "foobar")]
546+
#[case("foo / bar", "foo / bar")]
547+
#[case("foo\"bar", r#"foo\"bar"#)]
548+
#[case("foo\\bar", r"foo\\bar")]
549+
#[case("foo\x08\x0C\n\r\tbar", r"foo\b\f\n\r\tbar")]
550+
#[case("foo\x0B\x1B\x7Fbar", r"foo\u000b\u001b\u007fbar")]
551+
#[case("foo—bar", r"foo\u2014bar")]
552+
#[case("foo🐐bar", r"foo\ud83d\udc10bar")]
553+
fn test_write_json_str(#[case] s: &str, #[case] json: String) {
554+
let mut buf = String::new();
555+
write_json_str(s, &mut buf).unwrap();
556+
assert_eq!(buf, json);
557+
}
438558
}

0 commit comments

Comments
 (0)