use super::*;
#[test]
fn test_info_hash_from_hex() {
let hex = "0123456789abcdef0123456789abcdef01234567";
let hash = InfoHash::from_hex(hex).unwrap();
assert!(hash.is_v1());
assert_eq!(hash.to_hex(), hex);
}
#[test]
fn test_info_hash_v2() {
let hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let hash = InfoHash::from_hex(hex).unwrap();
assert!(hash.is_v2());
assert_eq!(hash.to_hex(), hex);
}
#[test]
fn test_magnet_link_parse() {
let uri = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=test&tr=http://tracker.example.com/announce";
let magnet = MagnetLink::parse(uri).unwrap();
assert!(magnet.info_hash.is_v1());
assert_eq!(magnet.display_name, Some("test".to_string()));
assert_eq!(magnet.trackers.len(), 1);
}
#[test]
fn test_magnet_link_roundtrip() {
let original = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=test";
let magnet = MagnetLink::parse(original).unwrap();
let uri = magnet.to_uri();
assert!(uri.contains("xt=urn:btih:0123456789abcdef0123456789abcdef01234567"));
assert!(uri.contains("dn=test"));
}
#[test]
fn test_merkle_tree() {
let hashes: Vec<[u8; 32]> = (0..4)
.map(|i| {
let mut h = [0u8; 32];
h[0] = i;
h
})
.collect();
let tree = MerkleTree::from_piece_hashes(hashes.clone());
assert!(tree.root().is_some());
let proof = tree.proof(0).unwrap();
let root = tree.root().unwrap();
assert!(MerkleTree::verify(&root, &hashes[0], 0, &proof));
}
#[test]
fn test_merkle_tree_single_leaf() {
let hash = [42u8; 32];
let tree = MerkleTree::from_piece_hashes(vec![hash]);
assert!(tree.root().is_some());
assert_eq!(tree.leaf_count(), 1);
}
#[test]
fn test_merkle_compute_root() {
use super::merkle::{compute_root, hash_block, hash_data_blocks, MERKLE_BLOCK_SIZE};
let data = vec![0xABu8; MERKLE_BLOCK_SIZE];
let root = compute_root(&data);
let expected_hash = hash_block(&data);
assert_eq!(root, expected_hash);
let data = vec![0xCDu8; MERKLE_BLOCK_SIZE * 2];
let root = compute_root(&data);
assert_ne!(root, [0u8; 32]);
let blocks = hash_data_blocks(&data);
assert_eq!(blocks.len(), 2);
}
#[test]
fn test_merkle_verify_piece() {
use super::merkle::{compute_root, verify_piece, MERKLE_BLOCK_SIZE};
let data = vec![0x42u8; MERKLE_BLOCK_SIZE * 4];
let root = compute_root(&data);
assert!(verify_piece(&data, &root));
let mut modified = data.clone();
modified[0] = 0xFF;
assert!(!verify_piece(&modified, &root));
let wrong_root = [0xFFu8; 32];
assert!(!verify_piece(&data, &wrong_root));
}
#[test]
fn test_merkle_verify_piece_layer() {
use super::merkle::{compute_piece_root, verify_piece_layer, MERKLE_BLOCK_SIZE};
let piece_length = (MERKLE_BLOCK_SIZE * 4) as u64;
let full_data = vec![0x42u8; piece_length as usize];
let root = compute_piece_root(&full_data, piece_length);
assert!(verify_piece_layer(&full_data, &root, piece_length));
let partial_len = MERKLE_BLOCK_SIZE * 2 + MERKLE_BLOCK_SIZE / 2;
let partial_data = vec![0x42u8; partial_len];
let partial_root = compute_piece_root(&partial_data, piece_length);
assert!(verify_piece_layer(
&partial_data,
&partial_root,
piece_length
));
assert!(!verify_piece_layer(&full_data, &partial_root, piece_length));
}
#[test]
fn test_merkle_extract_layer_hashes() {
use super::merkle::{extract_layer_hashes, MerkleTree};
let hashes: Vec<[u8; 32]> = (0..8u8)
.map(|i| {
let mut h = [0u8; 32];
h[0] = i;
h
})
.collect();
let tree = MerkleTree::from_piece_hashes(hashes.clone());
let layer0 = extract_layer_hashes(&tree, 0, 0, 4);
assert_eq!(layer0.len(), 4);
assert_eq!(layer0[0][0], 0);
assert_eq!(layer0[1][0], 1);
let layer1 = extract_layer_hashes(&tree, 1, 0, 2);
assert_eq!(layer1.len(), 2);
let empty = extract_layer_hashes(&tree, 0, 100, 10);
assert!(empty.is_empty());
}
#[test]
fn test_magnet_v2_parse() {
let v2_hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let uri = format!("magnet:?xt=urn:btmh:1220{}&dn=v2test", v2_hash);
let magnet = MagnetLink::parse(&uri).unwrap();
assert!(magnet.info_hash.is_v2());
assert_eq!(magnet.info_hash.to_hex(), v2_hash);
assert_eq!(magnet.display_name, Some("v2test".to_string()));
}
#[test]
fn test_magnet_hybrid_parse() {
let v1_hash = "0123456789abcdef0123456789abcdef01234567";
let v2_hash = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
let uri = format!(
"magnet:?xt=urn:btih:{}&xt=urn:btmh:1220{}&dn=hybrid",
v1_hash, v2_hash
);
let magnet = MagnetLink::parse(&uri).unwrap();
assert!(magnet.info_hash.is_hybrid());
assert_eq!(magnet.display_name, Some("hybrid".to_string()));
let v1 = magnet.info_hash.v1_hash().unwrap();
let v2 = magnet.info_hash.v2_hash().unwrap();
assert_eq!(v1.to_hex(), v1_hash);
assert_eq!(v2.to_hex(), v2_hash);
}
#[test]
fn test_magnet_hybrid_roundtrip() {
let v1_hash = "0123456789abcdef0123456789abcdef01234567";
let v2_hash = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
let uri = format!(
"magnet:?xt=urn:btih:{}&xt=urn:btmh:1220{}&dn=hybrid",
v1_hash, v2_hash
);
let magnet = MagnetLink::parse(&uri).unwrap();
let roundtrip = magnet.to_uri();
assert!(roundtrip.contains(&format!("xt=urn:btih:{}", v1_hash)));
assert!(roundtrip.contains(&format!("xt=urn:btmh:1220{}", v2_hash)));
assert!(roundtrip.contains("dn=hybrid"));
}
#[test]
fn test_piece_hashes_methods() {
let v1_hashes = vec![[1u8; 20], [2u8; 20]];
let v1 = PieceHashes::V1(v1_hashes.clone());
assert!(v1.has_v1());
assert!(!v1.has_v2());
assert_eq!(v1.v1_piece_count(), Some(2));
assert_eq!(v1.v1_pieces().unwrap().len(), 2);
assert!(v1.v2_layers().is_none());
let mut layers = std::collections::BTreeMap::new();
layers.insert([0xABu8; 32], vec![[1u8; 32], [2u8; 32]]);
let piece_layers = PieceLayers { layers };
let v2 = PieceHashes::V2(piece_layers);
assert!(!v2.has_v1());
assert!(v2.has_v2());
assert!(v2.v1_piece_count().is_none());
assert!(v2.v2_layers().is_some());
let mut layers2 = std::collections::BTreeMap::new();
layers2.insert([0xCDu8; 32], vec![[3u8; 32]]);
let hybrid = PieceHashes::Hybrid {
v1: vec![[4u8; 20]],
v2: PieceLayers { layers: layers2 },
};
assert!(hybrid.has_v1());
assert!(hybrid.has_v2());
assert_eq!(hybrid.v1_piece_count(), Some(1));
}
#[test]
fn test_torrent_version_methods() {
assert!(TorrentVersion::V1.supports_v1());
assert!(!TorrentVersion::V1.supports_v2());
assert!(!TorrentVersion::V2.supports_v1());
assert!(TorrentVersion::V2.supports_v2());
assert!(TorrentVersion::Hybrid.supports_v1());
assert!(TorrentVersion::Hybrid.supports_v2());
}
fn b(s: &[u8]) -> bytes::Bytes {
bytes::Bytes::copy_from_slice(s)
}
fn vb(s: &[u8]) -> bytes::Bytes {
bytes::Bytes::copy_from_slice(s)
}
#[test]
fn test_v2_torrent_structure() {
use std::collections::BTreeMap;
use crate::bencode::{encode, Value};
let piece_length: i64 = 16384; let file_content = vec![0xABu8; piece_length as usize];
let pieces_root = merkle::hash_block(&file_content);
let mut file_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(file_content.len() as i64));
attrs.insert(b(b"pieces root"), Value::Bytes(vb(&pieces_root)));
attrs
}),
);
let mut file_tree: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_tree.insert(b(b"test.txt"), Value::Dict(file_dict));
let mut info: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
info.insert(b(b"name"), Value::Bytes(vb(b"test_v2")));
info.insert(b(b"piece length"), Value::Integer(piece_length));
info.insert(b(b"meta version"), Value::Integer(2));
info.insert(b(b"file tree"), Value::Dict(file_tree));
let piece_layers: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
let mut torrent: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
torrent.insert(b(b"info"), Value::Dict(info));
torrent.insert(b(b"piece layers"), Value::Dict(piece_layers));
let encoded = encode(&Value::Dict(torrent)).unwrap();
let metainfo = Metainfo::from_bytes(&encoded).unwrap();
assert!(metainfo.version.supports_v2());
assert!(!metainfo.version.supports_v1());
assert_eq!(metainfo.info.name, "test_v2");
assert_eq!(metainfo.info.piece_length, piece_length as u64);
assert!(metainfo.info_hash.is_v2());
assert_eq!(metainfo.info.files.len(), 1);
assert_eq!(metainfo.info.files[0].length, file_content.len() as u64);
}
#[test]
fn test_v2_torrent_multi_file() {
use std::collections::BTreeMap;
use crate::bencode::{encode, Value};
let piece_length: i64 = 16384;
let file1_content = vec![0x11u8; piece_length as usize];
let file2_content = vec![0x22u8; piece_length as usize];
let file1_root = merkle::hash_block(&file1_content);
let file2_root = merkle::hash_block(&file2_content);
let mut file_tree: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
let mut file1_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file1_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(file1_content.len() as i64));
attrs.insert(b(b"pieces root"), Value::Bytes(vb(&file1_root)));
attrs
}),
);
file_tree.insert(b(b"file1.txt"), Value::Dict(file1_dict));
let mut file2_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file2_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(file2_content.len() as i64));
attrs.insert(b(b"pieces root"), Value::Bytes(vb(&file2_root)));
attrs
}),
);
file_tree.insert(b(b"file2.txt"), Value::Dict(file2_dict));
let mut info: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
info.insert(b(b"name"), Value::Bytes(vb(b"test_v2_multi")));
info.insert(b(b"piece length"), Value::Integer(piece_length));
info.insert(b(b"meta version"), Value::Integer(2));
info.insert(b(b"file tree"), Value::Dict(file_tree));
let mut torrent: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
torrent.insert(b(b"info"), Value::Dict(info));
torrent.insert(b(b"piece layers"), Value::Dict(BTreeMap::new()));
let encoded = encode(&Value::Dict(torrent)).unwrap();
let metainfo = Metainfo::from_bytes(&encoded).unwrap();
assert!(metainfo.version.supports_v2());
assert_eq!(metainfo.info.name, "test_v2_multi");
assert_eq!(metainfo.info.files.len(), 2);
let paths: Vec<_> = metainfo
.info
.files
.iter()
.map(|f| f.path.to_string_lossy().to_string())
.collect();
assert!(paths.iter().any(|p| p.contains("file1.txt")));
assert!(paths.iter().any(|p| p.contains("file2.txt")));
}
#[test]
fn test_hybrid_torrent_structure() {
use std::collections::BTreeMap;
use sha1::{Digest, Sha1};
use crate::bencode::{encode, Value};
let piece_length: i64 = 16384;
let file_content = vec![0xABu8; piece_length as usize];
let mut sha1_hasher = Sha1::new();
sha1_hasher.update(&file_content);
let v1_piece_hash: [u8; 20] = sha1_hasher.finalize().into();
let pieces_root = merkle::hash_block(&file_content);
let mut file_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(file_content.len() as i64));
attrs.insert(b(b"pieces root"), Value::Bytes(vb(&pieces_root)));
attrs
}),
);
let mut file_tree: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_tree.insert(b(b"test.txt"), Value::Dict(file_dict));
let mut info: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
info.insert(b(b"name"), Value::Bytes(vb(b"test_hybrid")));
info.insert(b(b"piece length"), Value::Integer(piece_length));
info.insert(b(b"length"), Value::Integer(file_content.len() as i64));
info.insert(b(b"pieces"), Value::Bytes(vb(&v1_piece_hash)));
info.insert(b(b"meta version"), Value::Integer(2));
info.insert(b(b"file tree"), Value::Dict(file_tree));
let mut torrent: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
torrent.insert(b(b"info"), Value::Dict(info));
torrent.insert(b(b"piece layers"), Value::Dict(BTreeMap::new()));
let encoded = encode(&Value::Dict(torrent)).unwrap();
let metainfo = Metainfo::from_bytes(&encoded).unwrap();
assert!(metainfo.version.supports_v1());
assert!(metainfo.version.supports_v2());
assert!(metainfo.info_hash.is_hybrid());
assert_eq!(metainfo.info.name, "test_hybrid");
assert!(metainfo.info_hash.v1_hash().is_some());
assert!(metainfo.info_hash.v2_hash().is_some());
}
#[test]
fn test_v2_piece_length_validation() {
use std::collections::BTreeMap;
use crate::bencode::{encode, Value};
let invalid_lengths = [
8192, 15000, 20000, ];
for piece_length in invalid_lengths {
let mut file_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(1000));
attrs.insert(b(b"pieces root"), Value::Bytes(vb(&[0u8; 32])));
attrs
}),
);
let mut file_tree: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_tree.insert(b(b"test.txt"), Value::Dict(file_dict));
let mut info: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
info.insert(b(b"name"), Value::Bytes(vb(b"test")));
info.insert(b(b"piece length"), Value::Integer(piece_length));
info.insert(b(b"meta version"), Value::Integer(2));
info.insert(b(b"file tree"), Value::Dict(file_tree));
let mut torrent: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
torrent.insert(b(b"info"), Value::Dict(info));
torrent.insert(b(b"piece layers"), Value::Dict(BTreeMap::new()));
let encoded = encode(&Value::Dict(torrent)).unwrap();
let result = Metainfo::from_bytes(&encoded);
assert!(
result.is_err(),
"Should reject piece_length {}",
piece_length
);
}
}
#[test]
fn test_v2_path_traversal_rejection() {
use std::collections::BTreeMap;
use crate::bencode::{encode, Value};
let mut file_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(16384));
attrs.insert(b(b"pieces root"), Value::Bytes(vb(&[0u8; 32])));
attrs
}),
);
let mut parent_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
parent_dict.insert(b(b"passwd"), Value::Dict(file_dict));
let mut file_tree: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_tree.insert(b(b".."), Value::Dict(parent_dict));
let mut info: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
info.insert(b(b"name"), Value::Bytes(vb(b"malicious")));
info.insert(b(b"piece length"), Value::Integer(16384));
info.insert(b(b"meta version"), Value::Integer(2));
info.insert(b(b"file tree"), Value::Dict(file_tree));
let mut torrent: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
torrent.insert(b(b"info"), Value::Dict(info));
torrent.insert(b(b"piece layers"), Value::Dict(BTreeMap::new()));
let encoded = encode(&Value::Dict(torrent)).unwrap();
let result = Metainfo::from_bytes(&encoded);
if let Ok(metainfo) = result {
let has_dotdot = metainfo.info.files.iter().any(|f| {
let path_str = f.path.to_string_lossy();
path_str.contains("..")
});
assert!(has_dotdot, "Path should contain '..' if parsing succeeded");
}
}
#[test]
fn test_v2_padding_file_detection() {
use std::collections::BTreeMap;
use crate::bencode::{encode, Value};
let piece_length: i64 = 16384;
let mut file_tree: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
let file_content = vec![0xABu8; piece_length as usize];
let file_root = merkle::hash_block(&file_content);
let mut file1_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file1_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(piece_length));
attrs.insert(b(b"pieces root"), Value::Bytes(vb(&file_root)));
attrs
}),
);
file_tree.insert(b(b"real.txt"), Value::Dict(file1_dict));
let mut pad_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
pad_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(0)); attrs.insert(b(b"attr"), Value::Bytes(vb(b"p"))); attrs
}),
);
file_tree.insert(b(b".pad"), Value::Dict(pad_dict));
let mut info: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
info.insert(b(b"name"), Value::Bytes(vb(b"with_padding")));
info.insert(b(b"piece length"), Value::Integer(piece_length));
info.insert(b(b"meta version"), Value::Integer(2));
info.insert(b(b"file tree"), Value::Dict(file_tree));
let mut torrent: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
torrent.insert(b(b"info"), Value::Dict(info));
torrent.insert(b(b"piece layers"), Value::Dict(BTreeMap::new()));
let encoded = encode(&Value::Dict(torrent)).unwrap();
let metainfo = Metainfo::from_bytes(&encoded).unwrap();
assert_eq!(metainfo.info.files.len(), 2);
let pad_file = metainfo.info.files.iter().find(|f| f.is_padding()).unwrap();
assert!(pad_file.is_padding());
assert_eq!(pad_file.length, 0);
}
#[test]
fn test_info_hash_v2_computation() {
use std::collections::BTreeMap;
use sha2::{Digest, Sha256};
use crate::bencode::{encode, Value};
let mut file_dict: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_dict.insert(
b(b""),
Value::Dict({
let mut attrs: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
attrs.insert(b(b"length"), Value::Integer(16384));
attrs.insert(b(b"pieces root"), Value::Bytes(vb(&[0xABu8; 32])));
attrs
}),
);
let mut file_tree: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
file_tree.insert(b(b"test.txt"), Value::Dict(file_dict));
let mut info: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
info.insert(b(b"name"), Value::Bytes(vb(b"hash_test")));
info.insert(b(b"piece length"), Value::Integer(16384));
info.insert(b(b"meta version"), Value::Integer(2));
info.insert(b(b"file tree"), Value::Dict(file_tree));
let info_encoded = encode(&Value::Dict(info.clone())).unwrap();
let mut hasher = Sha256::new();
hasher.update(&info_encoded);
let expected_hash: [u8; 32] = hasher.finalize().into();
let mut torrent: BTreeMap<bytes::Bytes, Value> = BTreeMap::new();
torrent.insert(b(b"info"), Value::Dict(info));
torrent.insert(b(b"piece layers"), Value::Dict(BTreeMap::new()));
let encoded = encode(&Value::Dict(torrent)).unwrap();
let metainfo = Metainfo::from_bytes(&encoded).unwrap();
let computed = metainfo.info_hash.v2_hash().unwrap();
assert_eq!(computed.as_bytes(), &expected_hash);
}