Skip to main content

irontide_core/
lib.rs

1#![warn(missing_docs)]
2//! Core `BitTorrent` types: info hashes, metadata, magnets, piece arithmetic, and torrent creation.
3
4mod crc32c;
5mod create;
6mod detect;
7mod error;
8mod file_priority;
9mod file_selection;
10mod file_tree;
11mod hash;
12mod hash_picker;
13mod hash_request;
14mod info_hashes;
15mod lengths;
16mod live_guard;
17mod magnet;
18mod merkle;
19mod merkle_state;
20mod metainfo;
21mod metainfo_v2;
22mod peer_id;
23mod resume_data;
24mod storage_mode;
25mod torrent_version;
26mod web_seed_stats;
27
28pub use crc32c::crc32c;
29pub use create::{CreateTorrent, CreateTorrentResult, auto_piece_size};
30pub use detect::{TorrentMeta, torrent_from_bytes_any};
31pub use error::{Error, Result};
32pub use file_priority::FilePriority;
33pub use file_selection::FileSelection;
34pub use file_tree::{FileTreeNode, V2FileAttr, V2FileInfo};
35pub use hash::{Id20, Id32};
36pub use hash_picker::{AddHashesResult, FileHashInfo, HashPicker};
37pub use hash_request::{HashRequest, validate_hash_request};
38pub use info_hashes::InfoHashes;
39pub use lengths::{DEFAULT_CHUNK_SIZE, Lengths};
40pub use live_guard::LiveConnectionGuard;
41pub use magnet::Magnet;
42pub use merkle::MerkleTree;
43pub use merkle_state::{MerkleTreeState, SetBlockResult};
44pub use metainfo::{FileEntry, FileInfo, InfoDict, TorrentMetaV1, torrent_from_bytes};
45pub use metainfo_v2::{InfoDictV2, TorrentMetaV2, torrent_v2_from_bytes};
46pub use peer_id::PeerId;
47pub use resume_data::{FastResumeData, UnfinishedPiece};
48pub use storage_mode::StorageMode;
49pub use torrent_version::TorrentVersion;
50pub use web_seed_stats::{WebSeedState, WebSeedStats};
51
52// Re-export Sha1Hasher at crate root (defined below with crypto cfg blocks).
53
54/// Network address family for dual-stack support.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum AddressFamily {
57    /// IPv4.
58    V4,
59    /// IPv6.
60    V6,
61}
62
63/// One step of an xorshift64 pseudo-random sequence.
64///
65/// Returns the next 64-bit state given the current state. Caller is
66/// responsible for seeding (state must be non-zero) and storing the
67/// returned value as the next state.
68///
69/// **Why expose this.** Throughout the workspace we deliberately avoid
70/// the `rand` dependency — see `crates/irontide/CLAUDE.md` ("Random
71/// bytes: thread-local xorshift64 seeded from `SystemTime`"). Several
72/// modules (peer ID generation, sim per-link RNG state) need
73/// reproducible 64-bit pseudo-randomness; consolidating the algorithm
74/// here removes ~3 cut-and-paste copies.
75#[must_use]
76pub fn xorshift64_step(mut state: u64) -> u64 {
77    state ^= state << 13;
78    state ^= state >> 7;
79    state ^= state << 17;
80    state
81}
82
83// --- Crypto backend: ring ---
84
85/// Compute SHA1 hash of input bytes.
86#[cfg(all(
87    feature = "crypto-ring",
88    not(feature = "crypto-openssl"),
89    not(feature = "crypto-aws-lc")
90))]
91pub fn sha1(data: &[u8]) -> Id20 {
92    let hash = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data);
93    let mut id = [0u8; 20];
94    id.copy_from_slice(hash.as_ref());
95    Id20(id)
96}
97
98/// Compute SHA1 hash of multiple chunks without concatenating them.
99///
100/// Avoids allocating a large buffer when piece data is stored as separate blocks.
101#[cfg(all(
102    feature = "crypto-ring",
103    not(feature = "crypto-openssl"),
104    not(feature = "crypto-aws-lc")
105))]
106pub fn sha1_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id20 {
107    let mut ctx = ring::digest::Context::new(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY);
108    for chunk in chunks {
109        ctx.update(chunk);
110    }
111    let hash = ctx.finish();
112    let mut id = [0u8; 20];
113    id.copy_from_slice(hash.as_ref());
114    Id20(id)
115}
116
117/// Compute SHA-256 hash of input bytes (used by BitTorrent v2, BEP 52).
118#[cfg(all(
119    feature = "crypto-ring",
120    not(feature = "crypto-openssl"),
121    not(feature = "crypto-aws-lc")
122))]
123pub fn sha256(data: &[u8]) -> Id32 {
124    let hash = ring::digest::digest(&ring::digest::SHA256, data);
125    let mut id = [0u8; 32];
126    id.copy_from_slice(hash.as_ref());
127    Id32(id)
128}
129
130/// Compute SHA-256 hash of multiple chunks without concatenating them.
131#[cfg(all(
132    feature = "crypto-ring",
133    not(feature = "crypto-openssl"),
134    not(feature = "crypto-aws-lc")
135))]
136pub fn sha256_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id32 {
137    let mut ctx = ring::digest::Context::new(&ring::digest::SHA256);
138    for chunk in chunks {
139        ctx.update(chunk);
140    }
141    let hash = ctx.finish();
142    let mut id = [0u8; 32];
143    id.copy_from_slice(hash.as_ref());
144    Id32(id)
145}
146
147// --- Crypto backend: openssl ---
148
149/// Compute SHA1 hash of input bytes.
150#[cfg(feature = "crypto-openssl")]
151pub fn sha1(data: &[u8]) -> Id20 {
152    let hash = openssl::hash::hash(openssl::hash::MessageDigest::sha1(), data).unwrap();
153    let mut id = [0u8; 20];
154    id.copy_from_slice(&hash);
155    Id20(id)
156}
157
158/// Compute SHA1 hash of multiple chunks without concatenating them.
159///
160/// Avoids allocating a large buffer when piece data is stored as separate blocks.
161#[cfg(feature = "crypto-openssl")]
162pub fn sha1_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id20 {
163    let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha1()).unwrap();
164    for chunk in chunks {
165        hasher.update(chunk).unwrap();
166    }
167    let hash = hasher.finish().unwrap();
168    let mut id = [0u8; 20];
169    id.copy_from_slice(&hash);
170    Id20(id)
171}
172
173/// Compute SHA-256 hash of input bytes (used by BitTorrent v2, BEP 52).
174#[cfg(feature = "crypto-openssl")]
175pub fn sha256(data: &[u8]) -> Id32 {
176    let hash = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), data).unwrap();
177    let mut id = [0u8; 32];
178    id.copy_from_slice(&hash);
179    Id32(id)
180}
181
182/// Compute SHA-256 hash of multiple chunks without concatenating them.
183#[cfg(feature = "crypto-openssl")]
184pub fn sha256_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id32 {
185    let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256()).unwrap();
186    for chunk in chunks {
187        hasher.update(chunk).unwrap();
188    }
189    let hash = hasher.finish().unwrap();
190    let mut id = [0u8; 32];
191    id.copy_from_slice(&hash);
192    Id32(id)
193}
194
195// --- Crypto backend: aws-lc-rs ---
196
197/// Compute SHA1 hash of input bytes.
198#[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
199#[must_use]
200pub fn sha1(data: &[u8]) -> Id20 {
201    let hash = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY, data);
202    let mut id = [0u8; 20];
203    id.copy_from_slice(hash.as_ref());
204    Id20(id)
205}
206
207/// Compute SHA1 hash of multiple chunks without concatenating them.
208///
209/// Avoids allocating a large buffer when piece data is stored as separate blocks.
210#[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
211pub fn sha1_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id20 {
212    let mut ctx = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY);
213    for chunk in chunks {
214        ctx.update(chunk);
215    }
216    let hash = ctx.finish();
217    let mut id = [0u8; 20];
218    id.copy_from_slice(hash.as_ref());
219    Id20(id)
220}
221
222/// Compute SHA-256 hash of input bytes (used by `BitTorrent` v2, BEP 52).
223#[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
224#[must_use]
225pub fn sha256(data: &[u8]) -> Id32 {
226    let hash = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, data);
227    let mut id = [0u8; 32];
228    id.copy_from_slice(hash.as_ref());
229    Id32(id)
230}
231
232/// Compute SHA-256 hash of multiple chunks without concatenating them.
233#[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
234pub fn sha256_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id32 {
235    let mut ctx = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
236    for chunk in chunks {
237        ctx.update(chunk);
238    }
239    let hash = ctx.finish();
240    let mut id = [0u8; 32];
241    id.copy_from_slice(hash.as_ref());
242    Id32(id)
243}
244
245// --- Incremental SHA-1 hasher for streaming verification ---
246
247/// Incremental SHA-1 hasher for streaming piece verification.
248///
249/// Eliminates per-piece allocation by allowing callers to feed data in
250/// fixed-size chunks through a reusable buffer rather than reading the
251/// entire piece into memory at once.
252pub struct Sha1Hasher {
253    #[cfg(all(
254        feature = "crypto-ring",
255        not(feature = "crypto-openssl"),
256        not(feature = "crypto-aws-lc")
257    ))]
258    ctx: ring::digest::Context,
259    #[cfg(feature = "crypto-openssl")]
260    ctx: openssl::hash::Hasher,
261    #[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
262    ctx: aws_lc_rs::digest::Context,
263}
264
265impl Sha1Hasher {
266    /// Create a new incremental SHA-1 hasher.
267    #[must_use]
268    pub fn new() -> Self {
269        Self {
270            #[cfg(all(
271                feature = "crypto-ring",
272                not(feature = "crypto-openssl"),
273                not(feature = "crypto-aws-lc")
274            ))]
275            ctx: ring::digest::Context::new(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
276            #[cfg(feature = "crypto-openssl")]
277            ctx: openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha1()).unwrap(),
278            #[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
279            ctx: aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY),
280        }
281    }
282
283    /// Feed data into the hasher.
284    pub fn update(&mut self, data: &[u8]) {
285        #[cfg(all(
286            feature = "crypto-ring",
287            not(feature = "crypto-openssl"),
288            not(feature = "crypto-aws-lc")
289        ))]
290        self.ctx.update(data);
291
292        #[cfg(feature = "crypto-openssl")]
293        self.ctx.update(data).unwrap();
294
295        #[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
296        self.ctx.update(data);
297    }
298
299    /// Finalize the hash and return the SHA-1 digest.
300    #[cfg(all(
301        feature = "crypto-ring",
302        not(feature = "crypto-openssl"),
303        not(feature = "crypto-aws-lc")
304    ))]
305    pub fn finish(self) -> Id20 {
306        let hash = self.ctx.finish();
307        let mut id = [0u8; 20];
308        id.copy_from_slice(hash.as_ref());
309        Id20(id)
310    }
311
312    /// Finalize the hash and return the SHA-1 digest.
313    #[cfg(feature = "crypto-openssl")]
314    pub fn finish(mut self) -> Id20 {
315        let hash = self.ctx.finish().unwrap();
316        let mut id = [0u8; 20];
317        id.copy_from_slice(&hash);
318        Id20(id)
319    }
320
321    /// Finalize the hash and return the SHA-1 digest.
322    #[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
323    #[must_use]
324    pub fn finish(self) -> Id20 {
325        let hash = self.ctx.finish();
326        let mut id = [0u8; 20];
327        id.copy_from_slice(hash.as_ref());
328        Id20(id)
329    }
330}
331
332impl Default for Sha1Hasher {
333    fn default() -> Self {
334        Self::new()
335    }
336}
337
338/// Fill a buffer with pseudo-random bytes (xorshift64, not cryptographic).
339pub fn random_bytes(buf: &mut [u8]) {
340    for b in buf.iter_mut() {
341        *b = peer_id::random_byte();
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn sha256_empty_string() {
351        let hash = sha256(b"");
352        assert_eq!(
353            hash.to_hex(),
354            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
355        );
356    }
357
358    #[test]
359    fn sha256_hello() {
360        let hash = sha256(b"hello");
361        assert_eq!(
362            hash.to_hex(),
363            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
364        );
365    }
366
367    #[test]
368    fn random_bytes_fills_buffer() {
369        let mut buf = [0u8; 32];
370        random_bytes(&mut buf);
371        // At least some bytes should be non-zero (probability of all-zero is ~0)
372        assert!(buf.iter().any(|&b| b != 0));
373    }
374
375    #[test]
376    fn sha1_hasher_matches_oneshot() {
377        let data = b"hello world, this is a streaming hash test";
378        let expected = sha1(data);
379
380        let mut hasher = Sha1Hasher::new();
381        hasher.update(&data[..12]);
382        hasher.update(&data[12..]);
383        assert_eq!(hasher.finish(), expected);
384    }
385
386    #[test]
387    fn sha1_hasher_empty() {
388        let expected = sha1(b"");
389        let hasher = Sha1Hasher::new();
390        assert_eq!(hasher.finish(), expected);
391    }
392}