Skip to content

Commit 64055d6

Browse files
authored
Merge pull request #28 from artrixdotdev/refactor/better-hashes
refactor: Move all info hashes to singular Hash struct
2 parents 809272c + dcb77f6 commit 64055d6

File tree

8 files changed

+481
-114
lines changed

8 files changed

+481
-114
lines changed

lib/src/hashes.rs

Lines changed: 402 additions & 0 deletions
Large diffs are not rendered by default.

lib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod errors;
2+
pub mod hashes;
23
pub mod parser;
34
pub mod tracker;

lib/src/parser/file.rs

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
use crate::{parser::MetaInfo, tracker::Tracker};
1+
use crate::{
2+
hashes::{Hash, HashVec, InfoHash},
3+
parser::MetaInfo,
4+
tracker::Tracker,
5+
};
26

37
use anyhow::Result;
4-
use serde::{
5-
Deserialize, Serialize, Serializer,
6-
de::{self, Visitor},
7-
};
8+
use serde::{Deserialize, Serialize};
89
use serde_bencode as bencode;
910
use sha1::{Digest, Sha1};
10-
use std::{fmt, path::PathBuf};
11+
use std::path::PathBuf;
1112

1213
use tokio::fs;
1314

@@ -44,7 +45,7 @@ pub struct Info {
4445
#[serde(rename = "piece length")]
4546
piece_length: u64,
4647
/// Binary string of concatenated 20-byte SHA-1 hash values
47-
pieces: Hashes,
48+
pieces: HashVec<20>,
4849
#[serde(flatten)]
4950
file: InfoKeys,
5051

@@ -75,62 +76,11 @@ pub struct InfoFile {
7576

7677
impl Info {
7778
/// Gets the file hash (xt, or exact topic) for the given Info struct
78-
pub fn hash(&self) -> Result<String> {
79+
pub fn hash(&self) -> Result<InfoHash> {
7980
let mut hasher = Sha1::new();
8081
hasher.update(serde_bencode::to_bytes(&self)?);
8182
let result = hasher.finalize();
82-
Ok(hex::encode(result))
83-
}
84-
}
85-
86-
/// A custom type for serializing and deserializing a vector of 20-byte SHA-1 hashes.
87-
/// Credit to [Jon Gjengset](https://github.com/jonhoo/codecrafters-bittorrent-rust/)
88-
#[derive(Debug)]
89-
pub struct Hashes(pub Vec<[u8; 20]>);
90-
91-
// Add this implementation
92-
impl<'de> Deserialize<'de> for Hashes {
93-
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
94-
where
95-
D: de::Deserializer<'de>,
96-
{
97-
deserializer.deserialize_bytes(HashesVisitor)
98-
}
99-
}
100-
101-
impl Serialize for Hashes {
102-
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103-
where
104-
S: Serializer,
105-
{
106-
let single_slice = self.0.concat();
107-
108-
serializer.serialize_bytes(&single_slice)
109-
}
110-
}
111-
112-
struct HashesVisitor;
113-
114-
impl Visitor<'_> for HashesVisitor {
115-
type Value = Hashes;
116-
117-
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
118-
formatter.write_str("a byte string whose length is a multiple of 20")
119-
}
120-
121-
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
122-
where
123-
E: de::Error,
124-
{
125-
if v.len() % 20 != 0 {
126-
return Err(E::custom(format!("length is {}", v.len())));
127-
}
128-
129-
Ok(Hashes(
130-
v.chunks(20)
131-
.map(|slice_20| slice_20.try_into().expect("guaranteed to be length 20"))
132-
.collect(),
133-
))
83+
Ok(Hash::from_hex(hex::encode(result))?)
13484
}
13585
}
13686

lib/src/parser/magnet.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use std::collections::HashMap;
22

3-
use crate::{parser::MetaInfo, tracker::Tracker};
3+
use crate::{
4+
hashes::{Hash, InfoHash},
5+
parser::MetaInfo,
6+
tracker::Tracker,
7+
};
48

59
use anyhow::Result;
610
use serde::Deserialize;
@@ -9,8 +13,9 @@ use serde_qs;
913
/// Magnet URI Spec: https://en.wikipedia.org/wiki/Magnet_URI_scheme or https://www.bittorrent.org/beps/bep_0053.html
1014
#[derive(Debug, Deserialize)]
1115
pub struct MagnetUri {
16+
/// use `Self::info_hash` to get the info hash as a `Hash` struct.
1217
#[serde(rename(deserialize = "xt"))]
13-
pub info_hash: String,
18+
info_hash: String,
1419

1520
#[serde(rename(deserialize = "dn"))]
1621
pub name: String,
@@ -82,6 +87,16 @@ impl MagnetUri {
8287
// Parse the modified query string
8388
Ok(MetaInfo::MagnetUri(serde_qs::from_str(&final_qs)?))
8489
}
90+
pub fn info_hash(&self) -> Result<InfoHash, anyhow::Error> {
91+
let hex_part = self
92+
.info_hash
93+
.split(":")
94+
.last()
95+
.ok_or_else(|| anyhow::anyhow!("Invalid info_hash format: no colon found"))?;
96+
97+
Hash::from_hex(hex_part)
98+
.map_err(|e| anyhow::anyhow!("Failed to parse info_hash from hex: {}", e))
99+
}
85100
}
86101

87102
#[cfg(test)]

lib/src/parser/mod.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ mod magnet;
55
pub use file::*;
66
pub use magnet::*;
77

8+
use crate::hashes::InfoHash;
9+
810
/// Always utilize MetaInfo instead of directly using TorrentFile or MagnetUri
911
#[derive(Debug, Deserialize)]
1012
pub enum MetaInfo {
@@ -17,12 +19,15 @@ impl MetaInfo {
1719
/// function will calculate and return the hash. If the enum is a [MagnetUri](MagnetUri), then this
1820
/// function will grab the existing hash and return it, as the MagnetUri spec already contains
1921
/// the hash.
20-
pub fn info_hash(&self) -> String {
22+
pub fn info_hash(&self) -> Result<InfoHash, anyhow::Error> {
2123
match &self {
22-
MetaInfo::Torrent(torrent) => torrent.info.hash().unwrap(),
23-
MetaInfo::MagnetUri(magnet_uri) => {
24-
String::from(magnet_uri.info_hash.split(":").last().unwrap())
25-
}
24+
MetaInfo::Torrent(torrent) => torrent
25+
.info
26+
.hash()
27+
.map_err(|e| anyhow::anyhow!("Failed to compute torrent info hash: {}", e)),
28+
MetaInfo::MagnetUri(magnet_uri) => magnet_uri
29+
.info_hash()
30+
.map_err(|e| anyhow::anyhow!("Failed to extract magnet URI info hash: {}", e)),
2631
}
2732
}
2833
}
@@ -43,8 +48,10 @@ mod tests {
4348
let contents = tokio::fs::read_to_string(path).await.unwrap();
4449

4550
let metainfo = MagnetUri::parse(contents).await.unwrap();
51+
52+
let info_hash = metainfo.info_hash().unwrap();
4653
assert_eq!(
47-
metainfo.info_hash(),
54+
info_hash.to_hex(),
4855
"dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c"
4956
);
5057
}
@@ -56,7 +63,12 @@ mod tests {
5663
.unwrap()
5764
.join("tests/torrents/big-buck-bunny.torrent");
5865
let file = TorrentFile::parse(path).await.unwrap();
59-
assert_eq!(file.info_hash(), "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c");
66+
67+
let info_hash = file.info_hash().unwrap();
68+
assert_eq!(
69+
info_hash.to_hex(),
70+
"dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c"
71+
);
6072
}
6173

6274
#[tokio::test]

lib/src/tracker/http.rs

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use super::{PeerAddr, TrackerTrait};
2-
use crate::errors::{HttpTrackerError, TrackerError};
2+
use crate::{
3+
errors::{HttpTrackerError, TrackerError},
4+
hashes::InfoHash,
5+
};
36
use rand::distr::{Alphanumeric, SampleString};
47
use serde::{
58
Deserialize, Serialize,
@@ -55,13 +58,13 @@ impl TrackerRequest {
5558
pub struct HttpTracker {
5659
uri: String,
5760
peer_id: String,
58-
info_hash: String,
61+
info_hash: InfoHash,
5962
params: TrackerRequest,
6063
}
6164

6265
impl HttpTracker {
6366
#[instrument(skip(info_hash), fields(uri = %uri))]
64-
pub fn new(uri: String, info_hash: String) -> HttpTracker {
67+
pub fn new(uri: String, info_hash: InfoHash) -> HttpTracker {
6568
let peer_id = Alphanumeric.sample_string(&mut rand::rng(), 20);
6669
debug!(peer_id = %peer_id, "Generated peer ID");
6770

@@ -93,27 +96,14 @@ impl TrackerTrait for HttpTracker {
9396
async fn stream_peers(&mut self) -> anyhow::Result<Vec<PeerAddr>> {
9497
// Decode info_hash
9598
debug!("Decoding info hash");
96-
let info_hash_part = self.info_hash.split("urn:btih:").last().ok_or_else(|| {
97-
HttpTrackerError::InvalidInfoHash(format!("Invalid info_hash format: {}", self.info_hash))
98-
})?;
99-
100-
let decoded_hash = hex::decode(info_hash_part).map_err(|e| {
101-
error!(error = %e, "Failed to decode info_hash");
102-
HttpTrackerError::InvalidInfoHash(format!("Failed to decode info_hash: {}", e))
103-
})?;
104-
105-
let info_hash: [u8; 20] = decoded_hash.try_into().map_err(|_| {
106-
error!("Info hash has incorrect length");
107-
HttpTrackerError::InvalidInfoHash("Info hash must be 20 bytes".to_string())
108-
})?;
10999

110100
// Generate params + URL. Specifically using the compact format by adding "compact=1" to
111101
// params.
112102
debug!("Generating request parameters");
113103
let params = serde_qs::to_string(&self.params)
114104
.map_err(|e| HttpTrackerError::ParameterEncoding(e.to_string()))?;
115105

116-
let info_hash_encoded = urlencode(&info_hash);
106+
let info_hash_encoded = urlencode(self.info_hash.as_bytes());
117107
trace!(encoded_hash = %info_hash_encoded, "URL-encoded info hash");
118108

119109
let uri_params = format!(
@@ -238,10 +228,10 @@ mod tests {
238228
let metainfo = MagnetUri::parse(contents).await.unwrap();
239229
match metainfo {
240230
MetaInfo::MagnetUri(magnet) => {
231+
let info_hash = magnet.info_hash();
241232
let announce_list = magnet.announce_list.unwrap();
242233
let announce_uri = announce_list[0].uri();
243-
let info_hash = magnet.info_hash;
244-
let mut http_tracker = HttpTracker::new(announce_uri, info_hash);
234+
let mut http_tracker = HttpTracker::new(announce_uri, info_hash.unwrap());
245235

246236
// Make request
247237
let res = HttpTracker::stream_peers(&mut http_tracker)

lib/src/tracker/mod.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use serde::{
66
};
77
use std::{fmt, net::Ipv4Addr};
88
use udp::UdpTracker;
9+
10+
use crate::hashes::InfoHash;
911
pub mod http;
1012
pub mod udp;
1113
// mod websocket;
@@ -37,21 +39,15 @@ pub enum Tracker {
3739
}
3840

3941
impl Tracker {
40-
pub async fn get_peers(&self, info_hash: String) -> Result<Vec<PeerAddr>> {
42+
pub async fn get_peers(&self, info_hash: InfoHash) -> Result<Vec<PeerAddr>> {
4143
match self {
4244
Tracker::Http(uri) => {
4345
let mut tracker = HttpTracker::new(uri.clone(), info_hash);
4446

4547
Ok(tracker.stream_peers().await.unwrap())
4648
}
4749
Tracker::Udp(uri) => {
48-
let mut tracker = UdpTracker::new(
49-
uri.clone(),
50-
None,
51-
hex::decode(info_hash).unwrap().try_into().unwrap(),
52-
)
53-
.await
54-
.unwrap();
50+
let mut tracker = UdpTracker::new(uri.clone(), None, info_hash).await.unwrap();
5551

5652
Ok(tracker.stream_peers().await.unwrap())
5753
}
@@ -68,18 +64,18 @@ impl Tracker {
6864
}
6965
}
7066

71-
struct AnnounceUriVisitor;
67+
struct TrackerVisitor;
7268

7369
impl<'de> Deserialize<'de> for Tracker {
7470
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7571
where
7672
D: serde::Deserializer<'de>,
7773
{
78-
deserializer.deserialize_string(AnnounceUriVisitor)
74+
deserializer.deserialize_string(TrackerVisitor)
7975
}
8076
}
8177

82-
impl Visitor<'_> for AnnounceUriVisitor {
78+
impl Visitor<'_> for TrackerVisitor {
8379
type Value = Tracker;
8480

8581
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {

0 commit comments

Comments
 (0)