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