1use std::fmt;
5use std::panic::Location;
6use std::str::FromStr;
7
8use aquatic_udp_protocol::{NumberOfBytes, PeerId};
9use thiserror::Error;
10use torrust_tracker_located_error::{Located, LocatedError};
11use torrust_tracker_primitives::info_hash::{self, InfoHash};
12use torrust_tracker_primitives::peer;
13
14use crate::servers::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
15use crate::servers::http::v1::query::{ParseQueryError, Query};
16use crate::servers::http::v1::responses;
17
18const INFO_HASH: &str = "info_hash";
20const PEER_ID: &str = "peer_id";
21const PORT: &str = "port";
22const DOWNLOADED: &str = "downloaded";
23const UPLOADED: &str = "uploaded";
24const LEFT: &str = "left";
25const EVENT: &str = "event";
26const COMPACT: &str = "compact";
27const NUMWANT: &str = "numwant";
28
29#[derive(Debug, PartialEq)]
60pub struct Announce {
61 pub info_hash: InfoHash,
64
65 pub peer_id: PeerId,
67
68 pub port: u16,
70
71 pub downloaded: Option<NumberOfBytes>,
74
75 pub uploaded: Option<NumberOfBytes>,
77
78 pub left: Option<NumberOfBytes>,
80
81 pub event: Option<Event>,
84
85 pub compact: Option<Compact>,
87
88 pub numwant: Option<u32>,
91}
92
93#[derive(Error, Debug)]
99pub enum ParseAnnounceQueryError {
100 #[error("missing query params for announce request in {location}")]
102 MissingParams { location: &'static Location<'static> },
103 #[error("missing param {param_name} in {location}")]
104 MissingParam {
105 location: &'static Location<'static>,
106 param_name: String,
107 },
108 #[error("invalid param value {param_value} for {param_name} in {location}")]
110 InvalidParam {
111 param_name: String,
112 param_value: String,
113 location: &'static Location<'static>,
114 },
115 #[error("param value overflow {param_value} for {param_name} in {location}")]
117 NumberOfBytesOverflow {
118 param_name: String,
119 param_value: String,
120 location: &'static Location<'static>,
121 },
122 #[error("invalid param value {param_value} for {param_name} in {source}")]
124 InvalidInfoHashParam {
125 param_name: String,
126 param_value: String,
127 source: LocatedError<'static, info_hash::ConversionError>,
128 },
129 #[error("invalid param value {param_value} for {param_name} in {source}")]
131 InvalidPeerIdParam {
132 param_name: String,
133 param_value: String,
134 source: LocatedError<'static, peer::IdConversionError>,
135 },
136}
137
138#[derive(PartialEq, Debug)]
146pub enum Event {
147 Started,
149 Stopped,
151 Completed,
154}
155
156impl FromStr for Event {
157 type Err = ParseAnnounceQueryError;
158
159 fn from_str(raw_param: &str) -> Result<Self, Self::Err> {
160 match raw_param {
161 "started" => Ok(Self::Started),
162 "stopped" => Ok(Self::Stopped),
163 "completed" => Ok(Self::Completed),
164 _ => Err(ParseAnnounceQueryError::InvalidParam {
165 param_name: EVENT.to_owned(),
166 param_value: raw_param.to_owned(),
167 location: Location::caller(),
168 }),
169 }
170 }
171}
172
173impl fmt::Display for Event {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 match self {
176 Event::Started => write!(f, "started"),
177 Event::Stopped => write!(f, "stopped"),
178 Event::Completed => write!(f, "completed"),
179 }
180 }
181}
182
183#[derive(PartialEq, Debug)]
193pub enum Compact {
194 Accepted = 1,
196 NotAccepted = 0,
199}
200
201impl fmt::Display for Compact {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 match self {
204 Compact::Accepted => write!(f, "1"),
205 Compact::NotAccepted => write!(f, "0"),
206 }
207 }
208}
209
210impl FromStr for Compact {
211 type Err = ParseAnnounceQueryError;
212
213 fn from_str(raw_param: &str) -> Result<Self, Self::Err> {
214 match raw_param {
215 "1" => Ok(Self::Accepted),
216 "0" => Ok(Self::NotAccepted),
217 _ => Err(ParseAnnounceQueryError::InvalidParam {
218 param_name: COMPACT.to_owned(),
219 param_value: raw_param.to_owned(),
220 location: Location::caller(),
221 }),
222 }
223 }
224}
225
226impl From<ParseQueryError> for responses::error::Error {
227 fn from(err: ParseQueryError) -> Self {
228 responses::error::Error {
229 failure_reason: format!("Cannot parse query params: {err}"),
230 }
231 }
232}
233
234impl From<ParseAnnounceQueryError> for responses::error::Error {
235 fn from(err: ParseAnnounceQueryError) -> Self {
236 responses::error::Error {
237 failure_reason: format!("Cannot parse query params for announce request: {err}"),
238 }
239 }
240}
241
242impl TryFrom<Query> for Announce {
243 type Error = ParseAnnounceQueryError;
244
245 fn try_from(query: Query) -> Result<Self, Self::Error> {
246 Ok(Self {
247 info_hash: extract_info_hash(&query)?,
248 peer_id: extract_peer_id(&query)?,
249 port: extract_port(&query)?,
250 downloaded: extract_downloaded(&query)?,
251 uploaded: extract_uploaded(&query)?,
252 left: extract_left(&query)?,
253 event: extract_event(&query)?,
254 compact: extract_compact(&query)?,
255 numwant: extract_numwant(&query)?,
256 })
257 }
258}
259
260fn extract_info_hash(query: &Query) -> Result<InfoHash, ParseAnnounceQueryError> {
263 match query.get_param(INFO_HASH) {
264 Some(raw_param) => {
265 Ok(
266 percent_decode_info_hash(&raw_param).map_err(|err| ParseAnnounceQueryError::InvalidInfoHashParam {
267 param_name: INFO_HASH.to_owned(),
268 param_value: raw_param.clone(),
269 source: Located(err).into(),
270 })?,
271 )
272 }
273 None => Err(ParseAnnounceQueryError::MissingParam {
274 location: Location::caller(),
275 param_name: INFO_HASH.to_owned(),
276 }),
277 }
278}
279
280fn extract_peer_id(query: &Query) -> Result<PeerId, ParseAnnounceQueryError> {
281 match query.get_param(PEER_ID) {
282 Some(raw_param) => Ok(
283 percent_decode_peer_id(&raw_param).map_err(|err| ParseAnnounceQueryError::InvalidPeerIdParam {
284 param_name: PEER_ID.to_owned(),
285 param_value: raw_param.clone(),
286 source: Located(err).into(),
287 })?,
288 ),
289 None => Err(ParseAnnounceQueryError::MissingParam {
290 location: Location::caller(),
291 param_name: PEER_ID.to_owned(),
292 }),
293 }
294}
295
296fn extract_port(query: &Query) -> Result<u16, ParseAnnounceQueryError> {
297 match query.get_param(PORT) {
298 Some(raw_param) => Ok(u16::from_str(&raw_param).map_err(|_e| ParseAnnounceQueryError::InvalidParam {
299 param_name: PORT.to_owned(),
300 param_value: raw_param.clone(),
301 location: Location::caller(),
302 })?),
303 None => Err(ParseAnnounceQueryError::MissingParam {
304 location: Location::caller(),
305 param_name: PORT.to_owned(),
306 }),
307 }
308}
309
310fn extract_downloaded(query: &Query) -> Result<Option<NumberOfBytes>, ParseAnnounceQueryError> {
313 extract_number_of_bytes_from_param(DOWNLOADED, query)
314}
315
316fn extract_uploaded(query: &Query) -> Result<Option<NumberOfBytes>, ParseAnnounceQueryError> {
317 extract_number_of_bytes_from_param(UPLOADED, query)
318}
319
320fn extract_left(query: &Query) -> Result<Option<NumberOfBytes>, ParseAnnounceQueryError> {
321 extract_number_of_bytes_from_param(LEFT, query)
322}
323
324fn extract_number_of_bytes_from_param(param_name: &str, query: &Query) -> Result<Option<NumberOfBytes>, ParseAnnounceQueryError> {
325 match query.get_param(param_name) {
326 Some(raw_param) => {
327 let number_of_bytes = u64::from_str(&raw_param).map_err(|_e| ParseAnnounceQueryError::InvalidParam {
328 param_name: param_name.to_owned(),
329 param_value: raw_param.clone(),
330 location: Location::caller(),
331 })?;
332
333 let number_of_bytes =
334 i64::try_from(number_of_bytes).map_err(|_e| ParseAnnounceQueryError::NumberOfBytesOverflow {
335 param_name: param_name.to_owned(),
336 param_value: raw_param.clone(),
337 location: Location::caller(),
338 })?;
339
340 let number_of_bytes = NumberOfBytes::new(number_of_bytes);
341
342 Ok(Some(number_of_bytes))
343 }
344 None => Ok(None),
345 }
346}
347
348fn extract_event(query: &Query) -> Result<Option<Event>, ParseAnnounceQueryError> {
349 match query.get_param(EVENT) {
350 Some(raw_param) => Ok(Some(Event::from_str(&raw_param)?)),
351 None => Ok(None),
352 }
353}
354
355fn extract_compact(query: &Query) -> Result<Option<Compact>, ParseAnnounceQueryError> {
356 match query.get_param(COMPACT) {
357 Some(raw_param) => Ok(Some(Compact::from_str(&raw_param)?)),
358 None => Ok(None),
359 }
360}
361
362fn extract_numwant(query: &Query) -> Result<Option<u32>, ParseAnnounceQueryError> {
363 match query.get_param(NUMWANT) {
364 Some(raw_param) => match u32::from_str(&raw_param) {
365 Ok(numwant) => Ok(Some(numwant)),
366 Err(_) => Err(ParseAnnounceQueryError::InvalidParam {
367 param_name: NUMWANT.to_owned(),
368 param_value: raw_param.clone(),
369 location: Location::caller(),
370 }),
371 },
372 None => Ok(None),
373 }
374}
375
376#[cfg(test)]
377mod tests {
378
379 mod announce_request {
380
381 use aquatic_udp_protocol::{NumberOfBytes, PeerId};
382 use torrust_tracker_primitives::info_hash::InfoHash;
383
384 use crate::servers::http::v1::query::Query;
385 use crate::servers::http::v1::requests::announce::{
386 Announce, Compact, Event, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, NUMWANT, PEER_ID, PORT, UPLOADED,
387 };
388
389 #[test]
390 fn should_be_instantiated_from_the_url_query_with_only_the_mandatory_params() {
391 let raw_query = Query::from(vec![
392 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
393 (PEER_ID, "-qB00000000000000001"),
394 (PORT, "17548"),
395 ])
396 .to_string();
397
398 let query = raw_query.parse::<Query>().unwrap();
399
400 let announce_request = Announce::try_from(query).unwrap();
401
402 assert_eq!(
403 announce_request,
404 Announce {
405 info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(),
406 peer_id: PeerId(*b"-qB00000000000000001"),
407 port: 17548,
408 downloaded: None,
409 uploaded: None,
410 left: None,
411 event: None,
412 compact: None,
413 numwant: None,
414 }
415 );
416 }
417
418 #[test]
419 fn should_be_instantiated_from_the_url_query_params() {
420 let raw_query = Query::from(vec![
421 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
422 (PEER_ID, "-qB00000000000000001"),
423 (PORT, "17548"),
424 (DOWNLOADED, "1"),
425 (UPLOADED, "2"),
426 (LEFT, "3"),
427 (EVENT, "started"),
428 (COMPACT, "0"),
429 (NUMWANT, "50"),
430 ])
431 .to_string();
432
433 let query = raw_query.parse::<Query>().unwrap();
434
435 let announce_request = Announce::try_from(query).unwrap();
436
437 assert_eq!(
438 announce_request,
439 Announce {
440 info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(),
441 peer_id: PeerId(*b"-qB00000000000000001"),
442 port: 17548,
443 downloaded: Some(NumberOfBytes::new(1)),
444 uploaded: Some(NumberOfBytes::new(2)),
445 left: Some(NumberOfBytes::new(3)),
446 event: Some(Event::Started),
447 compact: Some(Compact::NotAccepted),
448 numwant: Some(50),
449 }
450 );
451 }
452
453 mod when_it_is_instantiated_from_the_url_query_params {
454
455 use crate::servers::http::v1::query::Query;
456 use crate::servers::http::v1::requests::announce::{
457 Announce, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, NUMWANT, PEER_ID, PORT, UPLOADED,
458 };
459
460 #[test]
461 fn it_should_fail_if_the_query_does_not_include_all_the_mandatory_params() {
462 let raw_query_without_info_hash = "peer_id=-qB00000000000000001&port=17548";
463
464 assert!(Announce::try_from(raw_query_without_info_hash.parse::<Query>().unwrap()).is_err());
465
466 let raw_query_without_peer_id = "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&port=17548";
467
468 assert!(Announce::try_from(raw_query_without_peer_id.parse::<Query>().unwrap()).is_err());
469
470 let raw_query_without_port =
471 "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-qB00000000000000001";
472
473 assert!(Announce::try_from(raw_query_without_port.parse::<Query>().unwrap()).is_err());
474 }
475
476 #[test]
477 fn it_should_fail_if_the_info_hash_param_is_invalid() {
478 let raw_query = Query::from(vec![
479 (INFO_HASH, "INVALID_INFO_HASH_VALUE"),
480 (PEER_ID, "-qB00000000000000001"),
481 (PORT, "17548"),
482 ])
483 .to_string();
484
485 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
486 }
487
488 #[test]
489 fn it_should_fail_if_the_peer_id_param_is_invalid() {
490 let raw_query = Query::from(vec![
491 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
492 (PEER_ID, "INVALID_PEER_ID_VALUE"),
493 (PORT, "17548"),
494 ])
495 .to_string();
496
497 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
498 }
499
500 #[test]
501 fn it_should_fail_if_the_port_param_is_invalid() {
502 let raw_query = Query::from(vec![
503 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
504 (PEER_ID, "-qB00000000000000001"),
505 (PORT, "INVALID_PORT_VALUE"),
506 ])
507 .to_string();
508
509 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
510 }
511
512 #[test]
513 fn it_should_fail_if_the_downloaded_param_is_invalid() {
514 let raw_query = Query::from(vec![
515 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
516 (PEER_ID, "-qB00000000000000001"),
517 (PORT, "17548"),
518 (DOWNLOADED, "INVALID_DOWNLOADED_VALUE"),
519 ])
520 .to_string();
521
522 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
523 }
524
525 #[test]
526 fn it_should_fail_if_the_uploaded_param_is_invalid() {
527 let raw_query = Query::from(vec![
528 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
529 (PEER_ID, "-qB00000000000000001"),
530 (PORT, "17548"),
531 (UPLOADED, "INVALID_UPLOADED_VALUE"),
532 ])
533 .to_string();
534
535 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
536 }
537
538 #[test]
539 fn it_should_fail_if_the_left_param_is_invalid() {
540 let raw_query = Query::from(vec![
541 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
542 (PEER_ID, "-qB00000000000000001"),
543 (PORT, "17548"),
544 (LEFT, "INVALID_LEFT_VALUE"),
545 ])
546 .to_string();
547
548 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
549 }
550
551 #[test]
552 fn it_should_fail_if_the_event_param_is_invalid() {
553 let raw_query = Query::from(vec![
554 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
555 (PEER_ID, "-qB00000000000000001"),
556 (PORT, "17548"),
557 (EVENT, "INVALID_EVENT_VALUE"),
558 ])
559 .to_string();
560
561 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
562 }
563
564 #[test]
565 fn it_should_fail_if_the_compact_param_is_invalid() {
566 let raw_query = Query::from(vec![
567 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
568 (PEER_ID, "-qB00000000000000001"),
569 (PORT, "17548"),
570 (COMPACT, "INVALID_COMPACT_VALUE"),
571 ])
572 .to_string();
573
574 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
575 }
576
577 #[test]
578 fn it_should_fail_if_the_numwant_param_is_invalid() {
579 let raw_query = Query::from(vec![
580 (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
581 (PEER_ID, "-qB00000000000000001"),
582 (PORT, "17548"),
583 (NUMWANT, "-1"),
584 ])
585 .to_string();
586
587 assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
588 }
589 }
590 }
591}