use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use flate2::read::GzDecoder;
use log::debug;
use semver::{Version, VersionReq};
use serde::Deserialize;
use tar::Archive;
use crate::core::dependency::{DepKind, Dependency};
use crate::core::source::MaybePackage;
use crate::core::{InternedString, Package, PackageId, Source, SourceId, Summary};
use crate::sources::PathSource;
use crate::util::errors::CargoResultExt;
use crate::util::hex;
use crate::util::into_url::IntoUrl;
use crate::util::{CargoResult, Config, Filesystem};
const PACKAGE_SOURCE_LOCK: &str = ".cargo-ok";
pub const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
pub const CRATES_IO_REGISTRY: &str = "crates-io";
const CRATE_TEMPLATE: &str = "{crate}";
const VERSION_TEMPLATE: &str = "{version}";
pub struct RegistrySource<'cfg> {
source_id: SourceId,
src_path: Filesystem,
config: &'cfg Config,
updated: bool,
ops: Box<dyn RegistryData + 'cfg>,
index: index::RegistryIndex<'cfg>,
yanked_whitelist: HashSet<PackageId>,
}
#[derive(Deserialize)]
pub struct RegistryConfig {
pub dl: String,
pub api: Option<String>,
}
#[derive(Deserialize)]
pub struct RegistryPackage<'a> {
name: InternedString,
vers: Version,
#[serde(borrow)]
deps: Vec<RegistryDependency<'a>>,
features: BTreeMap<InternedString, Vec<InternedString>>,
cksum: String,
yanked: Option<bool>,
links: Option<InternedString>,
}
#[test]
fn escaped_char_in_json() {
let _: RegistryPackage<'_> = serde_json::from_str(
r#"{"name":"a","vers":"0.0.1","deps":[],"cksum":"bae3","features":{}}"#,
)
.unwrap();
let _: RegistryPackage<'_> = serde_json::from_str(
r#"{"name":"a","vers":"0.0.1","deps":[],"cksum":"bae3","features":{"test":["k","q"]},"links":"a-sys"}"#
).unwrap();
let _: RegistryPackage<'_> = serde_json::from_str(
r#"{
"name":"This name has a escaped cher in it \n\t\" ",
"vers":"0.0.1",
"deps":[{
"name": " \n\t\" ",
"req": " \n\t\" ",
"features": [" \n\t\" "],
"optional": true,
"default_features": true,
"target": " \n\t\" ",
"kind": " \n\t\" ",
"registry": " \n\t\" "
}],
"cksum":"bae3",
"features":{"test \n\t\" ":["k \n\t\" ","q \n\t\" "]},
"links":" \n\t\" "}"#,
)
.unwrap();
}
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Name,
Vers,
Deps,
Features,
Cksum,
Yanked,
Links,
}
#[derive(Deserialize)]
struct RegistryDependency<'a> {
name: InternedString,
#[serde(borrow)]
req: Cow<'a, str>,
features: Vec<InternedString>,
optional: bool,
default_features: bool,
target: Option<Cow<'a, str>>,
kind: Option<Cow<'a, str>>,
registry: Option<Cow<'a, str>>,
package: Option<InternedString>,
public: Option<bool>,
}
impl<'a> RegistryDependency<'a> {
pub fn into_dep(self, default: SourceId) -> CargoResult<Dependency> {
let RegistryDependency {
name,
req,
mut features,
optional,
default_features,
target,
kind,
registry,
package,
public,
} = self;
let id = if let Some(registry) = ®istry {
SourceId::for_registry(®istry.into_url()?)?
} else {
default
};
let mut dep = Dependency::parse_no_deprecated(package.unwrap_or(name), Some(&req), id)?;
if package.is_some() {
dep.set_explicit_name_in_toml(name);
}
let kind = match kind.as_deref().unwrap_or("") {
"dev" => DepKind::Development,
"build" => DepKind::Build,
_ => DepKind::Normal,
};
let platform = match target {
Some(target) => Some(target.parse()?),
None => None,
};
let public = public.unwrap_or(false);
features.retain(|s| !s.is_empty());
if !id.is_default_registry() {
dep.set_registry_id(id);
}
dep.set_optional(optional)
.set_default_features(default_features)
.set_features(features)
.set_platform(platform)
.set_kind(kind)
.set_public(public);
Ok(dep)
}
}
pub trait RegistryData {
fn prepare(&self) -> CargoResult<()>;
fn index_path(&self) -> &Filesystem;
fn load(
&self,
root: &Path,
path: &Path,
data: &mut dyn FnMut(&[u8]) -> CargoResult<()>,
) -> CargoResult<()>;
fn config(&mut self) -> CargoResult<Option<RegistryConfig>>;
fn update_index(&mut self) -> CargoResult<()>;
fn download(&mut self, pkg: PackageId, checksum: &str) -> CargoResult<MaybeLock>;
fn finish_download(&mut self, pkg: PackageId, checksum: &str, data: &[u8])
-> CargoResult<File>;
fn is_crate_downloaded(&self, _pkg: PackageId) -> bool {
true
}
fn assert_index_locked<'a>(&self, path: &'a Filesystem) -> &'a Path;
fn current_version(&self) -> Option<InternedString>;
}
pub enum MaybeLock {
Ready(File),
Download { url: String, descriptor: String },
}
mod index;
mod local;
mod remote;
fn short_name(id: SourceId) -> String {
let hash = hex::short_hash(&id);
let ident = id.url().host_str().unwrap_or("").to_string();
format!("{}-{}", ident, hash)
}
impl<'cfg> RegistrySource<'cfg> {
pub fn remote(
source_id: SourceId,
yanked_whitelist: &HashSet<PackageId>,
config: &'cfg Config,
) -> RegistrySource<'cfg> {
let name = short_name(source_id);
let ops = remote::RemoteRegistry::new(source_id, config, &name);
RegistrySource::new(source_id, config, &name, Box::new(ops), yanked_whitelist)
}
pub fn local(
source_id: SourceId,
path: &Path,
yanked_whitelist: &HashSet<PackageId>,
config: &'cfg Config,
) -> RegistrySource<'cfg> {
let name = short_name(source_id);
let ops = local::LocalRegistry::new(path, config, &name);
RegistrySource::new(source_id, config, &name, Box::new(ops), yanked_whitelist)
}
fn new(
source_id: SourceId,
config: &'cfg Config,
name: &str,
ops: Box<dyn RegistryData + 'cfg>,
yanked_whitelist: &HashSet<PackageId>,
) -> RegistrySource<'cfg> {
RegistrySource {
src_path: config.registry_source_path().join(name),
config,
source_id,
updated: false,
index: index::RegistryIndex::new(source_id, ops.index_path(), config),
yanked_whitelist: yanked_whitelist.clone(),
ops,
}
}
pub fn config(&mut self) -> CargoResult<Option<RegistryConfig>> {
self.ops.config()
}
fn unpack_package(&self, pkg: PackageId, tarball: &File) -> CargoResult<PathBuf> {
let package_dir = format!("{}-{}", pkg.name(), pkg.version());
let dst = self.src_path.join(&package_dir);
dst.create_dir()?;
let path = dst.join(PACKAGE_SOURCE_LOCK);
let path = self.config.assert_package_cache_locked(&path);
let unpack_dir = path.parent().unwrap();
if let Ok(meta) = path.metadata() {
if meta.len() > 0 {
return Ok(unpack_dir.to_path_buf());
}
}
let mut ok = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&path)?;
let gz = GzDecoder::new(tarball);
let mut tar = Archive::new(gz);
let prefix = unpack_dir.file_name().unwrap();
let parent = unpack_dir.parent().unwrap();
for entry in tar.entries()? {
let mut entry = entry.chain_err(|| "failed to iterate over archive")?;
let entry_path = entry
.path()
.chain_err(|| "failed to read entry path")?
.into_owned();
if !entry_path.starts_with(prefix) {
anyhow::bail!(
"invalid tarball downloaded, contains \
a file at {:?} which isn't under {:?}",
entry_path,
prefix
)
}
entry
.unpack_in(parent)
.chain_err(|| format!("failed to unpack entry at `{}`", entry_path.display()))?;
}
write!(ok, "ok")?;
Ok(unpack_dir.to_path_buf())
}
fn do_update(&mut self) -> CargoResult<()> {
self.ops.update_index()?;
let path = self.ops.index_path();
self.index = index::RegistryIndex::new(self.source_id, path, self.config);
self.updated = true;
Ok(())
}
fn get_pkg(&mut self, package: PackageId, path: &File) -> CargoResult<Package> {
let path = self
.unpack_package(package, path)
.chain_err(|| format!("failed to unpack package `{}`", package))?;
let mut src = PathSource::new(&path, self.source_id, self.config);
src.update()?;
let mut pkg = match src.download(package)? {
MaybePackage::Ready(pkg) => pkg,
MaybePackage::Download { .. } => unreachable!(),
};
let req = VersionReq::exact(package.version());
let summary_with_cksum = self
.index
.summaries(package.name(), &req, &mut *self.ops)?
.map(|s| s.summary.clone())
.next()
.expect("summary not found");
if let Some(cksum) = summary_with_cksum.checksum() {
pkg.manifest_mut()
.summary_mut()
.set_checksum(cksum.to_string());
}
Ok(pkg)
}
}
impl<'cfg> Source for RegistrySource<'cfg> {
fn query(&mut self, dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
if dep.source_id().precise().is_some() && !self.updated {
debug!("attempting query without update");
let mut called = false;
self.index
.query_inner(dep, &mut *self.ops, &self.yanked_whitelist, &mut |s| {
if dep.matches(&s) {
called = true;
f(s);
}
})?;
if called {
return Ok(());
} else {
debug!("falling back to an update");
self.do_update()?;
}
}
self.index
.query_inner(dep, &mut *self.ops, &self.yanked_whitelist, &mut |s| {
if dep.matches(&s) {
f(s);
}
})
}
fn fuzzy_query(&mut self, dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
self.index
.query_inner(dep, &mut *self.ops, &self.yanked_whitelist, f)
}
fn supports_checksums(&self) -> bool {
true
}
fn requires_precise(&self) -> bool {
false
}
fn source_id(&self) -> SourceId {
self.source_id
}
fn update(&mut self) -> CargoResult<()> {
if self.source_id.precise() != Some("locked") {
self.do_update()?;
} else {
debug!("skipping update due to locked registry");
}
Ok(())
}
fn download(&mut self, package: PackageId) -> CargoResult<MaybePackage> {
let hash = self.index.hash(package, &mut *self.ops)?;
match self.ops.download(package, hash)? {
MaybeLock::Ready(file) => self.get_pkg(package, &file).map(MaybePackage::Ready),
MaybeLock::Download { url, descriptor } => {
Ok(MaybePackage::Download { url, descriptor })
}
}
}
fn finish_download(&mut self, package: PackageId, data: Vec<u8>) -> CargoResult<Package> {
let hash = self.index.hash(package, &mut *self.ops)?;
let file = self.ops.finish_download(package, hash, &data)?;
self.get_pkg(package, &file)
}
fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
Ok(pkg.package_id().version().to_string())
}
fn describe(&self) -> String {
self.source_id.display_index()
}
fn add_to_yanked_whitelist(&mut self, pkgs: &[PackageId]) {
self.yanked_whitelist.extend(pkgs);
}
fn is_yanked(&mut self, pkg: PackageId) -> CargoResult<bool> {
if !self.updated {
self.do_update()?;
}
self.index.is_yanked(pkg, &mut *self.ops)
}
}