use bytes::{Buf, BufMut, BytesMut};
use speedy::{BigEndian, Readable, Writable};
use std::io::Cursor;
use tokio::io;
use tokio_util::codec::{Decoder, Encoder};
use crate::torrent::TorrentState;
#[derive(Debug, Clone, PartialEq)]
pub enum Message {
Quit,
NewTorrent(String),
TorrentState(Option<TorrentState>),
TogglePause([u8; 20]),
RequestTorrentState([u8; 20]),
PrintTorrentStatus,
}
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum MessageId {
NewTorrent = 1,
TorrentState = 2,
GetTorrentState = 3,
TogglePause = 4,
PrintTorrentStatus = 5,
}
impl TryFrom<u8> for MessageId {
type Error = io::Error;
fn try_from(k: u8) -> Result<Self, Self::Error> {
use MessageId::*;
match k {
k if k == NewTorrent as u8 => Ok(NewTorrent),
k if k == TorrentState as u8 => Ok(TorrentState),
k if k == GetTorrentState as u8 => Ok(GetTorrentState),
k if k == PrintTorrentStatus as u8 => Ok(PrintTorrentStatus),
k if k == TogglePause as u8 => Ok(TogglePause),
_ => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Unknown message id",
)),
}
}
}
#[derive(Debug)]
pub struct DaemonCodec;
impl Encoder<Message> for DaemonCodec {
type Error = io::Error;
fn encode(
&mut self,
item: Message,
buf: &mut BytesMut,
) -> Result<(), Self::Error> {
match item {
Message::NewTorrent(magnet) => {
let msg_len = 1 + magnet.len() as u32;
buf.put_u32(msg_len);
buf.put_u8(MessageId::NewTorrent as u8);
buf.extend_from_slice(magnet.as_bytes());
}
Message::TorrentState(torrent_info) => {
let info_bytes = match torrent_info {
Some(v) => v.write_to_vec_with_ctx(BigEndian {})?,
None => vec![],
};
let msg_len = 1 + info_bytes.len() as u32;
buf.put_u32(msg_len);
buf.put_u8(MessageId::TorrentState as u8);
buf.extend_from_slice(&info_bytes);
}
Message::RequestTorrentState(info_hash) => {
let msg_len = 1 + info_hash.len() as u32;
buf.put_u32(msg_len);
buf.put_u8(MessageId::GetTorrentState as u8);
buf.extend_from_slice(&info_hash);
}
Message::TogglePause(info_hash) => {
let msg_len = 1 + info_hash.len() as u32;
buf.put_u32(msg_len);
buf.put_u8(MessageId::TogglePause as u8);
buf.extend_from_slice(&info_hash);
}
Message::PrintTorrentStatus => {
let msg_len = 1;
buf.put_u32(msg_len);
buf.put_u8(MessageId::PrintTorrentStatus as u8);
}
Message::Quit => {
buf.put_u32(0);
}
}
Ok(())
}
}
impl Decoder for DaemonCodec {
type Item = Message;
type Error = io::Error;
fn decode(
&mut self,
buf: &mut BytesMut,
) -> Result<Option<Self::Item>, Self::Error> {
if buf.remaining() < 4 {
return Ok(None);
}
let mut tmp_buf = Cursor::new(&buf);
let msg_len = tmp_buf.get_u32() as usize;
tmp_buf.set_position(0);
if buf.remaining() >= 4 + msg_len {
buf.advance(4);
if msg_len == 0 {
return Ok(Some(Message::Quit));
}
} else {
tracing::trace!(
"Read buffer is {} bytes long but message is {} bytes long",
buf.remaining(),
msg_len
);
return Ok(None);
}
let msg_id = MessageId::try_from(buf.get_u8())?;
let msg = match msg_id {
MessageId::NewTorrent => {
let mut payload = vec![0u8; buf.remaining()];
buf.copy_to_slice(&mut payload);
Message::NewTorrent(String::from_utf8(payload).unwrap())
}
MessageId::TorrentState => {
let mut info: Option<TorrentState> = None;
if buf.has_remaining() {
let mut payload = vec![0u8; buf.remaining()];
buf.copy_to_slice(&mut payload);
info = TorrentState::read_from_buffer_with_ctx(
BigEndian {},
&payload,
)
.ok();
}
Message::TorrentState(info)
}
MessageId::TogglePause => {
let mut payload = [0u8; 20_usize];
buf.copy_to_slice(&mut payload);
Message::TogglePause(payload)
}
MessageId::PrintTorrentStatus => Message::PrintTorrentStatus,
MessageId::GetTorrentState => {
let mut payload = [0u8; 20_usize];
buf.copy_to_slice(&mut payload);
Message::RequestTorrentState(payload)
}
};
Ok(Some(msg))
}
}
#[cfg(test)]
mod tests {
use crate::torrent::TorrentStatus;
use super::*;
#[test]
fn new_torrent() {
let mut buf = BytesMut::new();
let msg = Message::NewTorrent("magnet:blabla".to_owned());
DaemonCodec.encode(msg, &mut buf).unwrap();
println!("encoded {buf:?}");
let msg = DaemonCodec.decode(&mut buf).unwrap().unwrap();
println!("decoded {msg:?}");
match msg {
Message::NewTorrent(magnet) => {
assert_eq!(magnet, "magnet:blabla".to_owned());
}
_ => panic!(),
}
}
#[test]
fn torrent_state() {
let info = TorrentState {
name: "Eesti".to_owned(),
stats: crate::torrent::Stats {
interval: 5,
leechers: 9,
seeders: 1,
},
status: TorrentStatus::Downloading,
downloaded: 999,
download_rate: 111,
uploaded: 44,
size: 9,
info_hash: [0u8; 20],
};
let a = info.write_to_vec_with_ctx(BigEndian {}).unwrap();
println!("encoding a {a:?}");
let mut buf = BytesMut::new();
let msg = Message::TorrentState(Some(info.clone()));
DaemonCodec.encode(msg, &mut buf).unwrap();
let msg = DaemonCodec.decode(&mut buf).unwrap().unwrap();
match msg {
Message::TorrentState(deserialized) => {
assert_eq!(deserialized, Some(info));
}
_ => panic!(),
}
let mut buf = BytesMut::new();
let msg = Message::TorrentState(None);
DaemonCodec.encode(msg, &mut buf).unwrap();
let msg = DaemonCodec.decode(&mut buf).unwrap().unwrap();
match msg {
Message::TorrentState(r) => {
assert_eq!(r, None);
}
_ => panic!(),
}
}
#[test]
fn request_torrent_state() {
let mut buf = BytesMut::new();
let msg = Message::RequestTorrentState([1u8; 20]);
DaemonCodec.encode(msg, &mut buf).unwrap();
println!("encoded {buf:?}");
let msg = DaemonCodec.decode(&mut buf).unwrap().unwrap();
println!("decoded {msg:?}");
match msg {
Message::RequestTorrentState(info_hash) => {
assert_eq!(info_hash, [1u8; 20]);
}
_ => panic!(),
}
}
}