irontide-core 1.1.0

Core types for BitTorrent: hashes, metainfo, magnets, piece arithmetic
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
#![warn(missing_docs)]
//! Core `BitTorrent` types: info hashes, metadata, magnets, piece arithmetic, and torrent creation.

mod config_types;
mod crc32c;
mod create;
mod detect;
mod error;
mod file_priority;
mod file_selection;
mod file_tree;
mod hash;
mod hash_picker;
mod hash_request;
mod info_hashes;
mod lengths;
mod live_guard;
mod magnet;
mod merkle;
mod merkle_state;
mod metainfo;
mod metainfo_v2;
mod net_util;
mod peer_id;
mod preallocate_mode;
mod resume_data;
mod storage_mode;
mod torrent_version;
mod web_seed_stats;

pub use config_types::{
    AlertCategory, ChokingAlgorithm, MixedModeAlgorithm, ProxyConfig, ProxyType,
    SeedChokingAlgorithm,
};
pub use crc32c::crc32c;
pub use create::{CreateTorrent, CreateTorrentResult, auto_piece_size};
pub use detect::{TorrentMeta, torrent_from_bytes_any};
pub use error::{Error, Result};
pub use file_priority::FilePriority;
pub use file_selection::FileSelection;
pub use file_tree::{FileTreeNode, V2FileAttr, V2FileInfo};
pub use hash::{Id20, Id32};
pub use hash_picker::{AddHashesResult, FileHashInfo, HashPicker};
pub use hash_request::{HashRequest, validate_hash_request};
pub use info_hashes::InfoHashes;
pub use lengths::{DEFAULT_CHUNK_SIZE, Lengths};
pub use live_guard::LiveConnectionGuard;
pub use magnet::Magnet;
pub use merkle::MerkleTree;
pub use merkle_state::{MerkleTreeState, SetBlockResult};
pub use metainfo::{
    ContentLayout, FileEntry, FileInfo, InfoDict, TorrentMetaV1, torrent_from_bytes,
};
pub use metainfo_v2::{InfoDictV2, TorrentMetaV2, torrent_v2_from_bytes};
pub use net_util::is_local_network;
pub use peer_id::PeerId;
pub use preallocate_mode::PreallocateMode;
pub use resume_data::{FastResumeData, UnfinishedPiece, validate_resume_bitfield};
pub use storage_mode::StorageMode;
pub use torrent_version::TorrentVersion;
pub use web_seed_stats::{WebSeedState, WebSeedStats};

// Re-export Sha1Hasher at crate root (defined below with crypto cfg blocks).

/// Network address family for dual-stack support.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AddressFamily {
    /// IPv4.
    V4,
    /// IPv6.
    V6,
}

/// One step of an xorshift64 pseudo-random sequence.
///
/// Returns the next 64-bit state given the current state. Caller is
/// responsible for seeding (state must be non-zero) and storing the
/// returned value as the next state.
///
/// **Why expose this.** Throughout the workspace we deliberately avoid
/// the `rand` dependency — see `crates/irontide/CLAUDE.md` ("Random
/// bytes: thread-local xorshift64 seeded from `SystemTime`"). Several
/// modules (peer ID generation, sim per-link RNG state) need
/// reproducible 64-bit pseudo-randomness; consolidating the algorithm
/// here removes ~3 cut-and-paste copies.
#[must_use]
pub fn xorshift64_step(mut state: u64) -> u64 {
    state ^= state << 13;
    state ^= state >> 7;
    state ^= state << 17;
    state
}

// --- Crypto backend: ring ---

/// Compute SHA1 hash of input bytes.
#[cfg(all(
    feature = "crypto-ring",
    not(feature = "crypto-openssl"),
    not(feature = "crypto-aws-lc")
))]
pub fn sha1(data: &[u8]) -> Id20 {
    let hash = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data);
    let mut id = [0u8; 20];
    id.copy_from_slice(hash.as_ref());
    Id20(id)
}

/// Compute SHA1 hash of multiple chunks without concatenating them.
///
/// Avoids allocating a large buffer when piece data is stored as separate blocks.
#[cfg(all(
    feature = "crypto-ring",
    not(feature = "crypto-openssl"),
    not(feature = "crypto-aws-lc")
))]
pub fn sha1_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id20 {
    let mut ctx = ring::digest::Context::new(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY);
    for chunk in chunks {
        ctx.update(chunk);
    }
    let hash = ctx.finish();
    let mut id = [0u8; 20];
    id.copy_from_slice(hash.as_ref());
    Id20(id)
}

/// Compute SHA-256 hash of input bytes (used by BitTorrent v2, BEP 52).
#[cfg(all(
    feature = "crypto-ring",
    not(feature = "crypto-openssl"),
    not(feature = "crypto-aws-lc")
))]
pub fn sha256(data: &[u8]) -> Id32 {
    let hash = ring::digest::digest(&ring::digest::SHA256, data);
    let mut id = [0u8; 32];
    id.copy_from_slice(hash.as_ref());
    Id32(id)
}

/// Compute SHA-256 hash of multiple chunks without concatenating them.
#[cfg(all(
    feature = "crypto-ring",
    not(feature = "crypto-openssl"),
    not(feature = "crypto-aws-lc")
))]
pub fn sha256_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id32 {
    let mut ctx = ring::digest::Context::new(&ring::digest::SHA256);
    for chunk in chunks {
        ctx.update(chunk);
    }
    let hash = ctx.finish();
    let mut id = [0u8; 32];
    id.copy_from_slice(hash.as_ref());
    Id32(id)
}

// --- Crypto backend: openssl ---

/// Compute SHA1 hash of input bytes.
#[cfg(feature = "crypto-openssl")]
pub fn sha1(data: &[u8]) -> Id20 {
    let hash = openssl::hash::hash(openssl::hash::MessageDigest::sha1(), data).unwrap();
    let mut id = [0u8; 20];
    id.copy_from_slice(&hash);
    Id20(id)
}

/// Compute SHA1 hash of multiple chunks without concatenating them.
///
/// Avoids allocating a large buffer when piece data is stored as separate blocks.
#[cfg(feature = "crypto-openssl")]
pub fn sha1_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id20 {
    let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha1()).unwrap();
    for chunk in chunks {
        hasher.update(chunk).unwrap();
    }
    let hash = hasher.finish().unwrap();
    let mut id = [0u8; 20];
    id.copy_from_slice(&hash);
    Id20(id)
}

/// Compute SHA-256 hash of input bytes (used by BitTorrent v2, BEP 52).
#[cfg(feature = "crypto-openssl")]
pub fn sha256(data: &[u8]) -> Id32 {
    let hash = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), data).unwrap();
    let mut id = [0u8; 32];
    id.copy_from_slice(&hash);
    Id32(id)
}

/// Compute SHA-256 hash of multiple chunks without concatenating them.
#[cfg(feature = "crypto-openssl")]
pub fn sha256_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id32 {
    let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256()).unwrap();
    for chunk in chunks {
        hasher.update(chunk).unwrap();
    }
    let hash = hasher.finish().unwrap();
    let mut id = [0u8; 32];
    id.copy_from_slice(&hash);
    Id32(id)
}

// --- Crypto backend: aws-lc-rs ---

/// Compute SHA1 hash of input bytes.
#[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
#[must_use]
pub fn sha1(data: &[u8]) -> Id20 {
    let hash = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY, data);
    let mut id = [0u8; 20];
    id.copy_from_slice(hash.as_ref());
    Id20(id)
}

/// Compute SHA1 hash of multiple chunks without concatenating them.
///
/// Avoids allocating a large buffer when piece data is stored as separate blocks.
#[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
pub fn sha1_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id20 {
    let mut ctx = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY);
    for chunk in chunks {
        ctx.update(chunk);
    }
    let hash = ctx.finish();
    let mut id = [0u8; 20];
    id.copy_from_slice(hash.as_ref());
    Id20(id)
}

/// Compute SHA-256 hash of input bytes (used by `BitTorrent` v2, BEP 52).
#[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
#[must_use]
pub fn sha256(data: &[u8]) -> Id32 {
    let hash = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, data);
    let mut id = [0u8; 32];
    id.copy_from_slice(hash.as_ref());
    Id32(id)
}

/// Compute SHA-256 hash of multiple chunks without concatenating them.
#[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
pub fn sha256_chunks<'a>(chunks: impl IntoIterator<Item = &'a [u8]>) -> Id32 {
    let mut ctx = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
    for chunk in chunks {
        ctx.update(chunk);
    }
    let hash = ctx.finish();
    let mut id = [0u8; 32];
    id.copy_from_slice(hash.as_ref());
    Id32(id)
}

// --- Incremental SHA-1 hasher for streaming verification ---

/// Incremental SHA-1 hasher for streaming piece verification.
///
/// Eliminates per-piece allocation by allowing callers to feed data in
/// fixed-size chunks through a reusable buffer rather than reading the
/// entire piece into memory at once.
pub struct Sha1Hasher {
    #[cfg(all(
        feature = "crypto-ring",
        not(feature = "crypto-openssl"),
        not(feature = "crypto-aws-lc")
    ))]
    ctx: ring::digest::Context,
    #[cfg(feature = "crypto-openssl")]
    ctx: openssl::hash::Hasher,
    #[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
    ctx: aws_lc_rs::digest::Context,
}

impl Sha1Hasher {
    /// Create a new incremental SHA-1 hasher.
    #[must_use]
    pub fn new() -> Self {
        Self {
            #[cfg(all(
                feature = "crypto-ring",
                not(feature = "crypto-openssl"),
                not(feature = "crypto-aws-lc")
            ))]
            ctx: ring::digest::Context::new(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
            #[cfg(feature = "crypto-openssl")]
            ctx: openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha1()).unwrap(),
            #[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
            ctx: aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY),
        }
    }

    /// Feed data into the hasher.
    pub fn update(&mut self, data: &[u8]) {
        #[cfg(all(
            feature = "crypto-ring",
            not(feature = "crypto-openssl"),
            not(feature = "crypto-aws-lc")
        ))]
        self.ctx.update(data);

        #[cfg(feature = "crypto-openssl")]
        self.ctx.update(data).unwrap();

        #[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
        self.ctx.update(data);
    }

    /// Finalize the hash and return the SHA-1 digest.
    #[cfg(all(
        feature = "crypto-ring",
        not(feature = "crypto-openssl"),
        not(feature = "crypto-aws-lc")
    ))]
    pub fn finish(self) -> Id20 {
        let hash = self.ctx.finish();
        let mut id = [0u8; 20];
        id.copy_from_slice(hash.as_ref());
        Id20(id)
    }

    /// Finalize the hash and return the SHA-1 digest.
    #[cfg(feature = "crypto-openssl")]
    pub fn finish(mut self) -> Id20 {
        let hash = self.ctx.finish().unwrap();
        let mut id = [0u8; 20];
        id.copy_from_slice(&hash);
        Id20(id)
    }

    /// Finalize the hash and return the SHA-1 digest.
    #[cfg(all(feature = "crypto-aws-lc", not(feature = "crypto-openssl")))]
    #[must_use]
    pub fn finish(self) -> Id20 {
        let hash = self.ctx.finish();
        let mut id = [0u8; 20];
        id.copy_from_slice(hash.as_ref());
        Id20(id)
    }
}

impl Default for Sha1Hasher {
    fn default() -> Self {
        Self::new()
    }
}

/// Fill a buffer with pseudo-random bytes (xorshift64, not cryptographic).
pub fn random_bytes(buf: &mut [u8]) {
    for b in buf.iter_mut() {
        *b = peer_id::random_byte();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sha256_empty_string() {
        let hash = sha256(b"");
        assert_eq!(
            hash.to_hex(),
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        );
    }

    #[test]
    fn sha256_hello() {
        let hash = sha256(b"hello");
        assert_eq!(
            hash.to_hex(),
            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
        );
    }

    #[test]
    fn random_bytes_fills_buffer() {
        let mut buf = [0u8; 32];
        random_bytes(&mut buf);
        // At least some bytes should be non-zero (probability of all-zero is ~0)
        assert!(buf.iter().any(|&b| b != 0));
    }

    #[test]
    fn sha1_hasher_matches_oneshot() {
        let data = b"hello world, this is a streaming hash test";
        let expected = sha1(data);

        let mut hasher = Sha1Hasher::new();
        hasher.update(&data[..12]);
        hasher.update(&data[12..]);
        assert_eq!(hasher.finish(), expected);
    }

    #[test]
    fn sha1_hasher_empty() {
        let expected = sha1(b"");
        let hasher = Sha1Hasher::new();
        assert_eq!(hasher.finish(), expected);
    }
}