use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::{env, str};
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
use toml_edit;
use crate::dependency::Dependency;
use crate::errors::*;
use semver::{Version, VersionReq};
const MANIFEST_FILENAME: &str = "Cargo.toml";
#[derive(Debug, Clone)]
pub struct Manifest {
pub data: toml_edit::Document,
}
pub fn find(specified: &Option<PathBuf>) -> Result<PathBuf> {
match *specified {
Some(ref path)
if fs::metadata(&path)
.chain_err(|| "Failed to get cargo file metadata")?
.is_file() =>
{
Ok(path.to_owned())
}
Some(ref path) => search(path),
None => search(&env::current_dir().chain_err(|| "Failed to get current directory")?),
}
}
fn search(dir: &Path) -> Result<PathBuf> {
let manifest = dir.join(MANIFEST_FILENAME);
if fs::metadata(&manifest).is_ok() {
Ok(manifest)
} else {
dir.parent()
.ok_or_else(|| ErrorKind::MissingManifest.into())
.and_then(|dir| search(dir))
}
}
fn merge_inline_table(old_dep: &mut toml_edit::Item, new: &toml_edit::Item) {
for (k, v) in new
.as_inline_table()
.expect("expected an inline table")
.iter()
{
old_dep[k] = toml_edit::value(v.clone());
}
}
fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
}
fn merge_dependencies(old_dep: &mut toml_edit::Item, new: &Dependency) {
assert!(!old_dep.is_none());
let new_toml = new.to_toml().1;
if str_or_1_len_table(old_dep) {
*old_dep = new_toml;
} else if old_dep.is_table_like() {
for key in &["version", "path", "git"] {
old_dep[key] = toml_edit::Item::None;
}
if let Some(name) = new_toml.as_str() {
old_dep["version"] = toml_edit::value(name);
} else {
merge_inline_table(old_dep, &new_toml);
}
} else {
unreachable!("Invalid old dependency type");
}
if let Some(t) = old_dep.as_inline_table_mut() {
t.fmt()
}
}
fn get_version(old_dep: &toml_edit::Item) -> Result<toml_edit::Item> {
if str_or_1_len_table(old_dep) {
Ok(old_dep.clone())
} else if old_dep.is_table_like() {
let version = old_dep["version"].clone();
if version.is_none() {
Err("Missing version field".into())
} else {
Ok(version)
}
} else {
unreachable!("Invalid old dependency type")
}
}
fn old_version_compatible(dependency: &Dependency, old_version: &str) -> Result<bool> {
let old_version = VersionReq::parse(old_version).chain_err(|| {
ErrorKind::ParseVersion(dependency.name.to_string(), old_version.to_string())
})?;
let current_version = match dependency.version() {
Some(current_version) => current_version,
None => return Ok(false),
};
let current_version = Version::parse(¤t_version).chain_err(|| {
ErrorKind::ParseVersion(dependency.name.to_string(), current_version.into())
})?;
Ok(old_version.matches(¤t_version))
}
fn print_upgrade_if_necessary(
crate_name: &str,
old_dep: &toml_edit::Item,
new_version: &toml_edit::Item,
) -> Result<()> {
let old_version = get_version(old_dep)?;
if let (Some(old_version), Some(new_version)) = (old_version.as_str(), new_version.as_str()) {
if old_version == new_version {
return Ok(());
}
let bufwtr = BufferWriter::stdout(ColorChoice::Always);
let mut buffer = bufwtr.buffer();
buffer
.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))
.chain_err(|| "Failed to set output colour")?;
write!(&mut buffer, " Upgrading ").chain_err(|| "Failed to write upgrade message")?;
buffer
.set_color(&ColorSpec::new())
.chain_err(|| "Failed to clear output colour")?;
writeln!(
&mut buffer,
"{} v{} -> v{}",
crate_name, old_version, new_version,
)
.chain_err(|| "Failed to write upgrade versions")?;
bufwtr
.print(&buffer)
.chain_err(|| "Failed to print upgrade message")?;
}
Ok(())
}
impl Manifest {
pub fn find_file(path: &Option<PathBuf>) -> Result<File> {
find(path).and_then(|path| {
OpenOptions::new()
.read(true)
.write(true)
.open(path)
.chain_err(|| "Failed to find Cargo.toml")
})
}
pub fn open(path: &Option<PathBuf>) -> Result<Manifest> {
let mut file = Manifest::find_file(path)?;
let mut data = String::new();
file.read_to_string(&mut data)
.chain_err(|| "Failed to read manifest contents")?;
data.parse().chain_err(|| "Unable to parse Cargo.toml")
}
pub fn get_table<'a>(&'a mut self, table_path: &[String]) -> Result<&'a mut toml_edit::Item> {
fn descend<'a>(
input: &'a mut toml_edit::Item,
path: &[String],
) -> Result<&'a mut toml_edit::Item> {
if let Some(segment) = path.get(0) {
let value = input[&segment].or_insert(toml_edit::table());
if value.is_table_like() {
descend(value, &path[1..])
} else {
Err(ErrorKind::NonExistentTable(segment.clone()).into())
}
} else {
Ok(input)
}
}
descend(&mut self.data.root, table_path)
}
pub fn get_sections(&self) -> Vec<(Vec<String>, toml_edit::Item)> {
let mut sections = Vec::new();
for dependency_type in &["dev-dependencies", "build-dependencies", "dependencies"] {
if self.data[dependency_type].is_table_like() {
sections.push((
vec![String::from(*dependency_type)],
self.data[dependency_type].clone(),
))
}
let target_sections = self
.data
.as_table()
.get("target")
.and_then(toml_edit::Item::as_table_like)
.into_iter()
.flat_map(toml_edit::TableLike::iter)
.filter_map(|(target_name, target_table)| {
let dependency_table = &target_table[dependency_type];
dependency_table.as_table_like().map(|_| {
(
vec![
"target".to_string(),
target_name.to_string(),
String::from(*dependency_type),
],
dependency_table.clone(),
)
})
});
sections.extend(target_sections);
}
sections
}
pub fn write_to_file(&self, file: &mut File) -> Result<()> {
if self.data["package"].is_none() && self.data["project"].is_none() {
if !self.data["workspace"].is_none() {
return Err(ErrorKind::UnexpectedRootManifest.into());
} else {
return Err(ErrorKind::InvalidManifest.into());
}
}
let s = self.data.to_string_in_original_order();
let new_contents_bytes = s.as_bytes();
file.set_len(new_contents_bytes.len() as u64)
.chain_err(|| "Failed to truncate Cargo.toml")?;
file.write_all(new_contents_bytes)
.chain_err(|| "Failed to write updated Cargo.toml")
}
pub fn insert_into_table(&mut self, table_path: &[String], dep: &Dependency) -> Result<()> {
let table = self.get_table(table_path)?;
let existing_dep = Self::find_dep(table, &dep.name);
if let Some((mut dep_name, dep_item)) = existing_dep {
if let Some(renamed) = dep.rename() {
table[renamed] = dep_item.clone();
table[&dep_name] = toml_edit::Item::None;
dep_name = renamed.to_owned();
} else if dep.name != dep_name {
table[&dep_name] = toml_edit::Item::None;
let (ref name, ref mut new_dependency) = dep.to_toml();
table[name] = new_dependency.clone();
dep_name = dep.name.to_owned();
}
merge_dependencies(&mut table[dep_name], dep);
if let Some(t) = table.as_inline_table_mut() {
t.fmt()
}
} else {
let (ref name, ref mut new_dependency) = dep.to_toml();
table[name] = new_dependency.clone();
}
Ok(())
}
pub fn update_table_entry(
&mut self,
table_path: &[String],
dep: &Dependency,
dry_run: bool,
) -> Result<()> {
self.update_table_named_entry(table_path, dep.name_in_manifest(), dep, dry_run)
}
pub fn update_table_named_entry(
&mut self,
table_path: &[String],
item_name: &str,
dep: &Dependency,
dry_run: bool,
) -> Result<()> {
let table = self.get_table(table_path)?;
let new_dep = dep.to_toml().1;
if !table[item_name].is_none() {
if let Err(e) = print_upgrade_if_necessary(&dep.name, &table[item_name], &new_dep) {
eprintln!("Error while displaying upgrade message, {}", e);
}
if !dry_run {
merge_dependencies(&mut table[item_name], dep);
if let Some(t) = table.as_inline_table_mut() {
t.fmt()
}
}
}
Ok(())
}
pub fn remove_from_table(&mut self, table: &str, name: &str) -> Result<()> {
if !self.data[table].is_table_like() {
return Err(ErrorKind::NonExistentTable(table.into()).into());
} else {
{
let dep = &mut self.data[table][name];
if dep.is_none() {
return Err(ErrorKind::NonExistentDependency(name.into(), table.into()).into());
}
*dep = toml_edit::Item::None;
}
if self.data[table].as_table_like().unwrap().is_empty() {
self.data[table] = toml_edit::Item::None;
}
}
Ok(())
}
pub fn add_deps(&mut self, table: &[String], deps: &[Dependency]) -> Result<()> {
deps.iter()
.map(|dep| self.insert_into_table(table, dep))
.collect::<Result<Vec<_>>>()?;
Ok(())
}
pub fn find_dep<'a>(
table: &'a mut toml_edit::Item,
dep_name: &'a str,
) -> Option<(String, &'a toml_edit::Item)> {
table
.as_table_like()
.unwrap()
.iter()
.find(|&item| match item {
(name, _) if name == dep_name => true,
(_alias, toml_edit::Item::Table(table_dep))
if table_dep.contains_key("package") =>
{
table_dep.get("package").unwrap().as_str() == Some(dep_name)
}
(_alias, toml_edit::Item::Value(toml_edit::Value::InlineTable(inline_dep)))
if inline_dep.contains_key("package") =>
{
inline_dep.get("package").unwrap().as_str() == Some(dep_name)
}
_ => false,
})
.map(|dep| (dep.0.into(), dep.1))
}
}
impl str::FromStr for Manifest {
type Err = Error;
fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
let d: toml_edit::Document = input.parse().chain_err(|| "Manifest not valid TOML")?;
Ok(Manifest { data: d })
}
}
#[derive(Debug)]
pub struct LocalManifest {
pub path: PathBuf,
manifest: Manifest,
}
impl Deref for LocalManifest {
type Target = Manifest;
fn deref(&self) -> &Manifest {
&self.manifest
}
}
impl LocalManifest {
pub fn find(path: &Option<PathBuf>) -> Result<Self> {
let path = find(path)?;
Self::try_new(&path)
}
pub fn try_new(path: &Path) -> Result<Self> {
let path = path.to_path_buf();
Ok(LocalManifest {
manifest: Manifest::open(&Some(path.clone()))?,
path,
})
}
fn get_file(&self) -> Result<File> {
Manifest::find_file(&Some(self.path.clone()))
}
pub fn upgrade(
&mut self,
dependency: &Dependency,
dry_run: bool,
skip_compatible: bool,
) -> Result<()> {
for (table_path, table) in self.get_sections() {
let table_like = table.as_table_like().expect("Unexpected non-table");
for (name, toml_item) in table_like.iter() {
let dep_name = toml_item
.as_table_like()
.and_then(|t| t.get("package").and_then(|p| p.as_str()))
.unwrap_or(name);
if dep_name == dependency.name {
if skip_compatible {
if let Some(old_version) = get_version(toml_item)?.as_str() {
if old_version_compatible(dependency, old_version)? {
continue;
}
}
}
self.manifest.update_table_named_entry(
&table_path,
&name,
dependency,
dry_run,
)?;
}
}
}
let mut file = self.get_file()?;
self.write_to_file(&mut file)
.chain_err(|| "Failed to write new manifest contents")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dependency::Dependency;
use toml_edit;
#[test]
fn add_remove_dependency() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let clone = manifest.clone();
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
let _ = manifest.insert_into_table(&["dependencies".to_owned()], &dep);
assert!(manifest
.remove_from_table("dependencies", &dep.name)
.is_ok());
assert_eq!(manifest.data.to_string(), clone.data.to_string());
}
#[test]
fn update_dependency() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
manifest
.insert_into_table(&["dependencies".to_owned()], &dep)
.unwrap();
let new_dep = Dependency::new("cargo-edit").set_version("0.2.0");
manifest
.update_table_entry(&["dependencies".to_owned()], &new_dep, false)
.unwrap();
}
#[test]
fn update_wrong_dependency() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
manifest
.insert_into_table(&["dependencies".to_owned()], &dep)
.unwrap();
let original = manifest.clone();
let new_dep = Dependency::new("wrong-dep").set_version("0.2.0");
manifest
.update_table_entry(&["dependencies".to_owned()], &new_dep, false)
.unwrap();
assert_eq!(manifest.data.to_string(), original.data.to_string());
}
#[test]
fn remove_dependency_no_section() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
assert!(manifest
.remove_from_table("dependencies", &dep.name)
.is_err());
}
#[test]
fn remove_dependency_non_existent() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
let other_dep = Dependency::new("other-dep").set_version("0.1.0");
let _ = manifest.insert_into_table(&["dependencies".to_owned()], &other_dep);
assert!(manifest
.remove_from_table("dependencies", &dep.name)
.is_err());
}
#[test]
fn old_version_is_compatible() -> Result<()> {
let with_version = Dependency::new("foo").set_version("2.3.4");
assert!(!old_version_compatible(&with_version, "1")?);
assert!(old_version_compatible(&with_version, "2")?);
assert!(!old_version_compatible(&with_version, "3")?);
Ok(())
}
#[test]
fn old_incompatible_with_missing_new_version() -> Result<()> {
let no_version = Dependency::new("foo");
assert!(!old_version_compatible(&no_version, "1")?);
assert!(!old_version_compatible(&no_version, "2")?);
Ok(())
}
#[test]
fn old_incompatible_with_invalid() {
let bad_version = Dependency::new("foo").set_version("CAKE CAKE");
let good_version = Dependency::new("foo").set_version("1.2.3");
assert!(old_version_compatible(&bad_version, "1").is_err());
assert!(old_version_compatible(&good_version, "CAKE CAKE").is_err());
}
}