1use crate::{
8 content::{CHUNK_SIZE, verify_chunk, verify_content},
9 ids::ContentId,
10 peer::PeerAddr,
11};
12
13#[derive(Debug, Clone)]
14pub struct ChunkProvider {
15 pub peer: PeerAddr,
16 pub content_bytes: Vec<u8>,
17}
18
19pub fn download_swarm(
20 content_id: [u8; 32],
21 chunk_hashes: &[[u8; 32]],
22 providers: &[ChunkProvider],
23) -> anyhow::Result<Vec<u8>> {
24 if providers.is_empty() {
25 anyhow::bail!("no providers available");
26 }
27
28 let mut output = Vec::new();
29 for (idx, expected_hash) in chunk_hashes.iter().enumerate() {
30 let mut chunk = None;
31
32 for offset in 0..providers.len() {
33 let provider_idx = (idx + offset) % providers.len();
34 if let Some(candidate) = chunk_from_provider(&providers[provider_idx], idx)
35 && verify_chunk(expected_hash, candidate).is_ok()
36 {
37 chunk = Some(candidate.to_vec());
38 break;
39 }
40 }
41
42 let Some(bytes) = chunk else {
43 anyhow::bail!("unable to retrieve verified chunk {idx}");
44 };
45 output.extend_from_slice(&bytes);
46 }
47
48 verify_content(&ContentId(content_id), &output)?;
49 Ok(output)
50}
51
52fn chunk_from_provider(provider: &ChunkProvider, idx: usize) -> Option<&[u8]> {
53 let start = idx * CHUNK_SIZE;
54 if start >= provider.content_bytes.len() {
55 return None;
56 }
57 let end = ((idx + 1) * CHUNK_SIZE).min(provider.content_bytes.len());
58 Some(&provider.content_bytes[start..end])
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::content::describe_content;
65 use crate::peer::TransportProtocol;
66
67 fn provider(ip: &str, bytes: Vec<u8>) -> ChunkProvider {
68 ChunkProvider {
69 peer: PeerAddr {
70 ip: ip.parse().expect("valid ip"),
71 port: 7000,
72 transport: TransportProtocol::Quic,
73 pubkey_hint: None,
74 relay_via: None,
75 },
76 content_bytes: bytes,
77 }
78 }
79
80 #[test]
81 fn swarm_download_verifies_and_recovers() {
82 let data = vec![1u8; CHUNK_SIZE + 17];
83 let desc = describe_content(&data);
84 let providers = vec![
85 provider("10.0.0.1", data.clone()),
86 provider("10.0.0.2", data.clone()),
87 ];
88
89 let out = download_swarm(desc.content_id.0, &desc.chunks, &providers).expect("download");
90 assert_eq!(out, data);
91 }
92
93 #[test]
94 fn swarm_download_rejects_corrupted_only_sources() {
95 let data = vec![2u8; CHUNK_SIZE + 3];
96 let desc = describe_content(&data);
97 let mut bad = data.clone();
98 bad[0] ^= 1;
99
100 let err = download_swarm(
101 desc.content_id.0,
102 &desc.chunks,
103 &[provider("10.0.0.3", bad)],
104 )
105 .expect_err("must fail");
106 assert!(err.to_string().contains("chunk 0"));
107 }
108}