use mtorrent_core::pwp::Bitfield;
use mtorrent_core::trackers::TrackerUrl;
use mtorrent_utils::benc::Element;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::io::Seek;
use std::path::Path;
use std::{fs, io};
const FILENAME_TRACKERS: &str = ".mtorrent_cfg";
const FILENAME_STATE: &str = ".mtorrent";
#[derive(Default, Serialize, Deserialize)]
struct Trackers {
trackers: BTreeSet<TrackerUrl>,
}
pub fn load_trackers(
config_dir: impl AsRef<Path>,
) -> io::Result<impl ExactSizeIterator<Item = TrackerUrl>> {
let file = fs::File::open(config_dir.as_ref().join(FILENAME_TRACKERS))?;
let Trackers { trackers } = serde_json::from_reader(io::BufReader::new(file))?;
Ok(trackers.into_iter())
}
pub fn save_trackers(
config_dir: impl AsRef<Path>,
trackers: impl IntoIterator<Item = TrackerUrl>,
) -> io::Result<()> {
let mut file = fs::File::options()
.write(true)
.read(true)
.create(true)
.truncate(false)
.append(false)
.open(config_dir.as_ref().join(FILENAME_TRACKERS))?;
let mut saved_trackers: Trackers =
serde_json::from_reader(io::BufReader::new(&file)).unwrap_or_default();
saved_trackers.trackers.extend(trackers);
file.seek(io::SeekFrom::Start(0))?;
serde_json::to_writer_pretty(io::BufWriter::new(&file), &saved_trackers)?;
Ok(())
}
pub fn remove_tracker(config_dir: impl AsRef<Path>, tracker: &TrackerUrl) -> io::Result<()> {
let mut file = fs::File::options()
.write(true)
.read(true)
.open(config_dir.as_ref().join(FILENAME_TRACKERS))?;
let mut saved_trackers: Trackers = serde_json::from_reader(io::BufReader::new(&file))?;
if !saved_trackers.trackers.remove(tracker) {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("tracker {tracker:?} not found in config"),
));
}
file.seek(io::SeekFrom::Start(0))?;
file.set_len(0)?;
serde_json::to_writer_pretty(io::BufWriter::new(&file), &saved_trackers)?;
Ok(())
}
pub fn load_state(config_dir: impl AsRef<Path>, info_hash: &[u8; 20]) -> io::Result<Bitfield> {
let buf = fs::read(config_dir.as_ref().join(FILENAME_STATE))?;
let bencode = Element::from_bytes(&buf)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "not bencoded"))?;
let Element::Dictionary(mut root) = bencode else {
return Err(io::Error::new(io::ErrorKind::InvalidData, "unexpected bencoded structure"));
};
let key = Element::ByteString(info_hash.into());
if let Some(Element::ByteString(bitfield)) = root.remove(&key) {
Ok(Bitfield::from_vec(bitfield))
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "info hash not found"))
}
}
pub fn save_state(
config_dir: impl AsRef<Path>,
info_hash: &[u8; 20],
state: Bitfield,
) -> io::Result<()> {
let key = Element::ByteString(info_hash.into());
let value = Element::ByteString(state.into_vec());
let root: BTreeMap<Element, Element> = [(key, value)].into();
let bencode = Element::Dictionary(root);
let config_path = config_dir.as_ref().join(FILENAME_STATE);
fs::write(config_path, bencode.encode())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_write_and_read_state_for_single_torrent() {
let dir = "test_write_and_read_state_for_single_torrent";
fs::create_dir_all(dir).unwrap();
let info_hash = [b'a'; 20];
let piece_count = 668;
let bitfield = Bitfield::repeat(true, piece_count);
assert!(
matches!(load_state(dir, &info_hash), Err(e) if e.kind() == io::ErrorKind::NotFound)
);
save_state(dir, &info_hash, bitfield.clone()).unwrap();
assert!(Path::new(dir).join(FILENAME_STATE).is_file());
let mut loaded_state = load_state(dir, &info_hash).unwrap();
loaded_state.resize(bitfield.len(), false);
assert_eq!(bitfield, loaded_state);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn test_read_trackers() {
let dir = "test_read_trackers";
fs::create_dir_all(dir).unwrap();
fs::write(
Path::new(dir).join(FILENAME_TRACKERS),
r#"{
"trackers": [
"http://tracker1.com",
"http://tracker2.com"
]
}"#,
)
.unwrap();
let loaded_trackers = load_trackers(dir).unwrap();
assert_eq!(
["http://tracker1.com", "http://tracker2.com"]
.into_iter()
.map(|s| TrackerUrl::from_str(s).unwrap())
.collect::<BTreeSet<_>>(),
loaded_trackers.collect::<BTreeSet<_>>(),
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn test_write_trackers() {
let dir = "test_write_trackers";
fs::create_dir_all(dir).unwrap();
save_trackers(
dir,
["http://tracker1.com", "http://tracker2.com"]
.into_iter()
.map(|s| TrackerUrl::from_str(s).unwrap()),
)
.unwrap();
let content = fs::read_to_string(Path::new(dir).join(FILENAME_TRACKERS)).unwrap();
assert_eq!(
content,
r#"{
"trackers": [
"http://tracker1.com",
"http://tracker2.com"
]
}"#,
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn test_remove_tracker() {
let dir = "test_remove_tracker";
fs::create_dir_all(dir).unwrap();
fs::write(
Path::new(dir).join(FILENAME_TRACKERS),
r#"{
"trackers": [
"http://tracker1.com",
"http://tracker2.com"
]
}"#,
)
.unwrap();
remove_tracker(dir, &"http://tracker1.com".parse().unwrap()).unwrap();
let content = fs::read_to_string(Path::new(dir).join(FILENAME_TRACKERS)).unwrap();
assert_eq!(
content,
r#"{
"trackers": [
"http://tracker2.com"
]
}"#,
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn test_write_and_read_and_modify_trackers() {
let dir = "test_write_and_read_and_modify_trackers";
fs::create_dir_all(dir).unwrap();
assert!(matches!(load_trackers(dir), Err(e) if e.kind() == io::ErrorKind::NotFound));
let initial_trackers = ["http://tracker1.com", "http://tracker2.com"]
.into_iter()
.map(|s| TrackerUrl::from_str(s).unwrap());
save_trackers(dir, initial_trackers.clone()).unwrap();
assert!(Path::new(dir).join(FILENAME_TRACKERS).is_file());
let loaded_trackers = load_trackers(dir).unwrap();
assert_eq!(
initial_trackers.clone().collect::<BTreeSet<_>>(),
loaded_trackers.collect::<BTreeSet<_>>(),
);
let new_trackers = ["http://tracker3.com", "http://tracker4.com"]
.into_iter()
.map(|s| TrackerUrl::from_str(s).unwrap());
save_trackers(dir, new_trackers.clone()).unwrap();
let loaded_trackers = load_trackers(dir).unwrap();
assert_eq!(
initial_trackers.clone().chain(new_trackers.clone()).collect::<BTreeSet<_>>(),
loaded_trackers.collect::<BTreeSet<_>>(),
);
for tracker in initial_trackers {
remove_tracker(dir, &tracker).unwrap();
}
let loaded_trackers = load_trackers(dir).unwrap();
assert_eq!(new_trackers.collect::<BTreeSet<_>>(), loaded_trackers.collect::<BTreeSet<_>>(),);
fs::remove_dir_all(dir).unwrap();
}
}