Ya tenemos el codigo mas difícil del MVP, descargar los bloques/piezas independientes según su indice. Si ahora hacemos un bucle for y descargamos todos los indices, guardamos en disco, ya hemos terminado.
spoiler pub async fn download_piece(
self,
stream: &mut TcpStream,
piece_index: u32,
) -> anyhow::Result<Vec<u8>> {
let piece_length = self
.torrent
.piece_length
.min(self.torrent.length - (self.torrent.piece_length * piece_index as i64));
let mut piece: Vec<u8> = Vec::with_capacity(piece_length as usize);
let mut begin_offset: u32 = 0;
let mut remain: u32 = piece_length as u32;
while remain != 0 {
let block_size = BLOCK_MAX_SIZE.min(remain);
PeerMessage::Request {
index: piece_index,
begin: begin_offset,
length: block_size,
}
.send(stream)
.await?;
if let PeerMessage::Piece {
index,
begin,
block,
} = PeerMessage::receive(stream).await?
{
assert_eq!(piece_index, index);
assert_eq!(begin_offset, begin);
piece.splice(begin as usize..begin as usize, block.into_iter());
} else {
bail!("expected piece message from peer")
}
begin_offset += block_size;
remain -= block_size as u32;
}
let piece_hash = self
.torrent
.pieces
.get(piece_index as usize)
.context("invalid index for piece hash")?;
let current_hash: [u8; 20] = {
let mut hasher = Sha1::new();
hasher.update(&piece);
hasher.finalize().into()
};
if *piece_hash != current_hash {
bail!(
"want: {}, got: {}",
hex::encode(piece_hash),
hex::encode(current_hash)
)
}
Ok(piece)
}
He estado 1h30min o asi en picarlo, mientras me dibujaba en la tablet los paquetes como cojones tenia que ordenarlos y como partir los bloques, un lio... muy fácil cagarla. Solo la cague en una linea de codigo, y no me daban los hashes pero revisando el codigo por suerte lo vi rapido... ademas el codigo que he hecho funciona para el caso de 1 archivo, que pasa si el torrent es 1 carpeta? no se si mi codigo esta bien o tiene errores...
piece.splice(begin as usize..begin as usize, block.into_iter());
Esta linea en concreto la lie, estaba guardando los bloques mal en mi piece resultante... por suerte el codigo se lee muy fácil y se entiende bien... y lo he visto en 5-10 minutos... Este codigo es el típico como hagas guarradas o no sepas bien lo que estes haciendo y te pongas a copiar cosas de ChatGPT y tengas un off-one error o alguna mierda no lo arreglas en dias XD
A nivel de codigo, he hecho otro refactor, el tema de buffers para no mandar múltiples paquetes tcp:
pub async fn send(&self, stream: &mut TcpStream) -> anyhow::Result<()> {
let mut buffer = Vec::new();
match self {
PeerMessage::Choke => todo!(),
PeerMessage::Unchoke => todo!(),
PeerMessage::Interested => {
buffer.write_u8(2).await?;
}
PeerMessage::NotInterested => todo!(),
PeerMessage::Have => todo!(),
PeerMessage::Bitfield(_) => todo!(),
PeerMessage::Request {
index,
begin,
length,
} => {
buffer.write_u8(6).await?;
buffer.write_u32(*index).await?;
buffer.write_u32(*begin).await?;
buffer.write_u32(*length).await?;
}
PeerMessage::Piece {
index,
begin,
block,
} => todo!(),
PeerMessage::Cancel => todo!(),
}
stream.write_u32(buffer.len() as u32).await?;
stream.write_all(&buffer).await?;
Ok(())
}
Como veis inicializo un buffer como os comente atras, antes estaba escribiendo directo al fd, y ahora en ese buffer meto todo, y luego al final si que escribo en el fd del stream la longitud del buffer y el buffer, que podria hacerlo en 1 pero bueno, una micro - optimización para deberes. Confío en el kernel. Para quien no lo sepa el kernel tiene sus propios buffers internos y seguramente esto se encarga de mandarlo en un paquete, lo podríamos debuggar mirando las los paquetes que mandamos por network.
Y mi main se ve tal que asi, he cambiado los if let por un match asi tengo en el error el paseo que ha fallado... aun no tengo el diseño final y estoy probando a mejorar alguna cosa cuando la veo a ver como se siente usar la api:
spoileruse anyhow::bail;
mod http;
mod torrent;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let path = "sample.torrent";
let torrent: torrent::Torrent = torrent::Torrent::from_path(path)?;
let announce_response = http::try_announce(&torrent).await?;
assert!(!announce_response.peers.is_empty());
let peer = announce_response
.peers
.get(0)
.expect("expected one peer at least");
let mut peer_stream = torrent::HandshakeMessage::new(torrent.info_hash_bytes)
.send(peer)
.await?
.receive()
.await?;
dbg!(&peer_stream);
match torrent::PeerMessage::receive(&mut peer_stream.stream).await? {
torrent::PeerMessage::Bitfield(payload) => {
dbg!(&payload);
}
other => {
bail!("expected: Bitfield, got:{:?}", other);
}
}
torrent::PeerMessage::Interested
.send(&mut peer_stream.stream)
.await?;
dbg!("interested send to peer");
match torrent::PeerMessage::receive(&mut peer_stream.stream).await? {
torrent::PeerMessage::Unchoke => {
dbg!("unchoke received");
}
other => {
bail!("expected: Unchoke, got:{:?}", other);
}
}
let piece = torrent.download_piece(&mut peer_stream.stream, 0).await?;
dbg!("piece downloaded successfully");
Ok(())
}
Una asignatura pendiente que me queda al final es ponerme a testear, que de momento todo lo estoy testando con integracion corriendo el main, pero molaría tener tests unitarios mas pequeños, sobretodo nos ayudara a limpiar el codigo una mas y no hacer chorradas. Y si ahora me pongo a hacer cosas mas complejas de peer (server) o descargar carpetas, estoy seguro que algun error tonto he cometido que aun no me ha saltado.