use std::{
cmp::Ordering,
convert::Infallible,
io::{BufRead, Cursor, Write},
ops::RangeInclusive,
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};
use console::style;
use futures::StreamExt;
use git2::Repository;
use indicatif::{ProgressBar, ProgressFinish, ProgressStyle};
use reqwest::Client;
use warp::http::HeaderValue;
use crate::{
crates::{get_crate_path, sync_one_crate_entry, CrateEntry},
download::DownloadError,
mirror::{default_user_agent, ConfigCrates, ConfigMirror, MirrorError},
progress_bar::padded_prefix_message,
};
static CRATES_403: [(&str, &str); 23] = [
("glib-2-0-sys", "0.0.1"),
("glib-2-0-sys", "0.0.2"),
("glib-2-0-sys", "0.0.3"),
("glib-2-0-sys", "0.0.4"),
("glib-2-0-sys", "0.0.5"),
("glib-2-0-sys", "0.0.6"),
("glib-2-0-sys", "0.0.7"),
("glib-2-0-sys", "0.0.8"),
("glib-2-0-sys", "0.1.0"),
("glib-2-0-sys", "0.1.1"),
("glib-2-0-sys", "0.1.2"),
("glib-2-0-sys", "0.2.0"),
("gobject-2-0-sys", "0.0.2"),
("gobject-2-0-sys", "0.0.3"),
("gobject-2-0-sys", "0.0.2"),
("gobject-2-0-sys", "0.0.4"),
("gobject-2-0-sys", "0.0.5"),
("gobject-2-0-sys", "0.0.6"),
("gobject-2-0-sys", "0.0.7"),
("gobject-2-0-sys", "0.0.8"),
("gobject-2-0-sys", "0.0.9"),
("gobject-2-0-sys", "0.1.0"),
("gobject-2-0-sys", "0.2.0"),
];
#[derive(Debug, PartialEq, Eq)]
enum Input {
Range(RangeInclusive<usize>),
Vec(Vec<usize>),
Usize(usize),
Ignore,
}
impl Input {
fn check(&self, length: usize) -> bool {
match self {
Input::Range(range) => *range.end() < length,
Input::Usize(u) => *u < length,
Input::Vec(v) => v.iter().all(|u| *u < length),
Input::Ignore => false,
}
}
}
impl FromStr for Input {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() || (s.contains(' ') && s.contains('-')) {
Ok(Self::Ignore)
} else if s.contains(' ') {
let mut result: Vec<usize> = s
.split(' ')
.filter_map(|s| match s.parse() {
Ok(u) if u != 0 => Some(u),
_ => None,
})
.collect();
if result.len() == 1 {
Ok(Self::Usize(result[0] - 1))
} else if !result.is_empty() {
result.sort_unstable();
result.iter_mut().for_each(|u| *u -= 1);
Ok(Self::Vec(result))
} else {
Ok(Self::Ignore)
}
} else if s.contains('-') {
let bounds: Vec<usize> = s
.split('-')
.filter_map(|s| match s.parse::<usize>() {
Ok(u) if u != 0 => Some(u),
_ => None,
})
.collect();
if bounds.len() == 2 {
let start = bounds[0] - 1;
let end = bounds[1] - 1;
match start.cmp(&end) {
Ordering::Less => Ok(Self::Range(RangeInclusive::new(start, end))),
Ordering::Equal => Ok(Self::Usize(start)),
Ordering::Greater => Ok(Self::Ignore),
}
} else {
Ok(Self::Ignore)
}
} else {
s.parse::<usize>().map_or(Ok(Self::Ignore), |u| {
if u == 0 {
Ok(Self::Ignore)
} else {
Ok(Self::Usize(u - 1))
}
})
}
}
}
pub(crate) async fn verify_mirror(
path: std::path::PathBuf,
current_step: &mut usize,
steps: usize,
) -> Result<Option<Vec<CrateEntry>>, MirrorError> {
let repo_path = path.join("crates.io-index");
if !repo_path.join(".git").exists() {
eprintln!("No index repository found in {}.", repo_path.display())
}
let prefix = padded_prefix_message(
*current_step,
steps,
"Comparing local crates.io and mirror coherence",
);
let pb = ProgressBar::new_spinner()
.with_style(
ProgressStyle::default_bar()
.template("{prefix} {wide_bar} {spinner} [{elapsed_precise}]")
.expect("Something went wrong with the template.")
.progress_chars(" "),
)
.with_prefix(prefix)
.with_finish(ProgressFinish::AndLeave);
pb.enable_steady_tick(Duration::from_millis(10));
let repo = Repository::open(repo_path)?;
let master = repo.find_reference("refs/heads/master")?;
let master_tree = master.peel_to_tree()?;
let diff = repo.diff_tree_to_tree(None, Some(&master_tree), None)?;
let mut missing_crates = Vec::new();
diff.foreach(
&mut |delta, _| {
let df = delta.new_file();
let p = df.path().unwrap();
if p == Path::new("config.json") {
return true;
}
if p.starts_with(".github/") {
return true;
}
let oid = df.id();
if oid.is_zero() {
return true;
}
let blob = repo.find_blob(oid).unwrap();
let data = blob.content();
for line in Cursor::new(data).lines() {
let line = line.unwrap();
let crate_entry: CrateEntry = match serde_json::from_str(&line) {
Ok(c) => c,
Err(_) => {
continue;
}
};
let file_path =
get_crate_path(&path, crate_entry.get_name(), crate_entry.get_vers()).unwrap();
if !CRATES_403
.iter()
.any(|it| it.0 == crate_entry.get_name() && it.1 == crate_entry.get_vers())
&& !crate_entry.get_yanked()
&& !file_path.exists()
{
missing_crates.push(crate_entry);
}
}
true
},
None,
None,
None,
)?;
pb.finish();
*current_step += 1;
if !missing_crates.is_empty() {
return Ok(Some(missing_crates));
}
eprintln!("{}", style("Verification successful.").bold());
Ok(None)
}
pub(crate) async fn handle_user_input(
mut missing_crates: Vec<CrateEntry>,
) -> Result<Vec<CrateEntry>, MirrorError> {
println!("Found {} missing crates:", missing_crates.len());
missing_crates.iter().enumerate().for_each(|(i, c)| {
println!(
" {}: {} - version {}",
style((i + 1).to_string()).bold(),
c.get_name(),
c.get_vers()
);
});
println!("{}",
style("Missing crates to download (e.g.: '1 2 3' or '1-3') [Leave empty for downloading all of them]:").bold()
);
std::io::stdout().flush()?;
let mut input = String::new();
match std::io::stdin().read_line(&mut input)? {
0 => Ok(missing_crates),
_ => {
input.pop();
let input = input.parse::<Input>().unwrap();
if input.check(missing_crates.len()) {
Ok(Vec::new())
} else {
match input {
Input::Ignore => Ok(missing_crates),
Input::Range(range) => {
range.into_iter().for_each(|u| {
missing_crates.remove(u);
});
Ok(missing_crates)
}
Input::Usize(u) => Ok(vec![missing_crates.remove(u)]),
Input::Vec(v) => {
v.into_iter().for_each(|u| {
missing_crates.remove(u);
});
Ok(missing_crates)
}
}
}
}
}
}
pub(crate) async fn fix_mirror(
mirror_config: &ConfigMirror,
crates_config: &ConfigCrates,
path: PathBuf,
crates_to_fetch: Vec<CrateEntry>,
current_step: &mut usize,
steps: usize,
) -> Result<(), MirrorError> {
let prefix = padded_prefix_message(*current_step, steps, "Repairing mirror");
let pb = ProgressBar::new(crates_to_fetch.len() as u64)
.with_style(
ProgressStyle::default_bar()
.template(
"{prefix} {wide_bar} {pos}/{len} [{elapsed_precise} / {duration_precise}]",
)
.expect("Something went wrong with the template.")
.progress_chars("█▉▊▋▌▍▎▏ "),
)
.with_prefix(prefix)
.with_finish(ProgressFinish::AndLeave);
pb.enable_steady_tick(Duration::from_millis(10));
let crates_source = if crates_config.source != "https://crates.io/api/v1/crates" {
Some(crates_config.source.as_str())
} else {
None
};
let user_agent_str =
mirror_config
.contact
.as_ref()
.map_or_else(default_user_agent, |contact| {
if contact != "your@email.com" {
format!("Panamax/{} ({})", env!("CARGO_PKG_VERSION"), contact)
} else {
default_user_agent()
}
});
let user_agent = match HeaderValue::from_str(&user_agent_str) {
Ok(h) => h,
Err(e) => {
eprintln!("Your contact information contains invalid characters!");
eprintln!("It's recommended to use a URL or email address as contact information.");
eprintln!("{e:?}");
return Ok(());
}
};
let client = Client::new();
let tasks = futures::stream::iter(crates_to_fetch.into_iter())
.map(|c| {
let client = client.clone();
let path = path.clone();
let mirror_retries = mirror_config.retries;
let crates_source = crates_source.map(|s| s.to_string());
let user_agent = user_agent.to_owned();
let pb = pb.clone();
tokio::spawn(async move {
let out = sync_one_crate_entry(
&client,
&path,
crates_source.as_deref(),
mirror_retries,
&c,
&user_agent,
)
.await;
pb.inc(1);
out
})
})
.buffer_unordered(crates_config.download_threads)
.collect::<Vec<_>>()
.await;
for t in tasks {
let res = t.unwrap();
match res {
Ok(())
| Err(DownloadError::NotFound {
status: _,
url: _,
data: _,
})
| Err(DownloadError::MismatchedHash {
expected: _,
actual: _,
}) => {}
Err(e) => {
eprintln!("Downloading failed: {e:?}");
}
}
}
pb.finish_and_clear();
*current_step += 1;
Ok(())
}
#[cfg(test)]
mod test {
mod input {
use crate::verify::Input;
#[test]
fn true_range() {
let input = "1-5".to_string();
let expected_result = Input::Range(0usize..=4);
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn false_range_true_usize() {
let input = "1-1".to_string();
let expected_result = Input::Usize(0);
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_range_1() {
let input = "foo-bar".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_range_2() {
let input = "1-5 7".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_range_3() {
let input = "1-foo".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_range_4() {
let input = "5-1".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_range_5() {
let input = "0-2".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_range_6() {
let input = "0-0".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn true_vec() {
let input = "1 2 5 9".to_string();
let expected_result = Input::Vec(vec![0, 1, 4, 8]);
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn true_vec_shuffled() {
let input = "6 4 8 2".to_string();
let expected_result = Input::Vec(vec![1, 3, 5, 7]);
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_vec() {
let input = "foo bar fubar".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn some_garbage_vec_1() {
let input = "1 bar 6".to_string();
let expected_result = Input::Vec(vec![0, 5]);
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn some_garbage_vec_2() {
let input = "0 2 6".to_string();
let expected_result = Input::Vec(vec![1, 5]);
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn some_garbage_vec_3() {
let input = "4 0 2".to_string();
let expected_result = Input::Vec(vec![1, 3]);
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn true_usize() {
let input = "42".to_string();
let expected_result = Input::Usize(41);
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_usize_1() {
let input = "foo".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn garbage_usize_2() {
let input = "0".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
#[test]
fn full_garbage() {
let input = "1-3 42".to_string();
let expected_result = Input::Ignore;
let result = input.parse::<Input>().unwrap();
assert_eq!(expected_result, result);
}
}
}