use std::fs;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::{env, str};
use semver::Version;
use super::errors::*;
use super::metadata::find_manifest_path;
#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Debug, Copy)]
pub enum DepKind {
Normal,
Development,
Build,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DepTable {
kind: DepKind,
target: Option<String>,
}
impl DepTable {
const KINDS: &'static [Self] = &[
Self::new().set_kind(DepKind::Normal),
Self::new().set_kind(DepKind::Development),
Self::new().set_kind(DepKind::Build),
];
pub(crate) const fn new() -> Self {
Self {
kind: DepKind::Normal,
target: None,
}
}
pub(crate) const fn set_kind(mut self, kind: DepKind) -> Self {
self.kind = kind;
self
}
pub(crate) fn set_target(mut self, target: impl Into<String>) -> Self {
self.target = Some(target.into());
self
}
fn kind_table(&self) -> &str {
match self.kind {
DepKind::Normal => "dependencies",
DepKind::Development => "dev-dependencies",
DepKind::Build => "build-dependencies",
}
}
}
impl Default for DepTable {
fn default() -> Self {
Self::new()
}
}
impl From<DepKind> for DepTable {
fn from(other: DepKind) -> Self {
Self::new().set_kind(other)
}
}
#[derive(Debug, Clone)]
pub struct Manifest {
pub data: toml_edit::Document,
}
impl Manifest {
pub(crate) fn get_table_mut<'a>(
&'a mut self,
table_path: &[String],
) -> CargoResult<&'a mut toml_edit::Item> {
self.get_table_mut_internal(table_path, false)
}
pub(crate) fn get_sections(&self) -> Vec<(DepTable, toml_edit::Item)> {
let mut sections = Vec::new();
for table in DepTable::KINDS {
let dependency_type = table.kind_table();
if self
.data
.get(dependency_type)
.map(|t| t.is_table_like())
.unwrap_or(false)
{
sections.push((table.clone(), 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.get(dependency_type)?;
dependency_table.as_table_like().map(|_| {
(
table.clone().set_target(target_name),
dependency_table.clone(),
)
})
});
sections.extend(target_sections);
}
sections
}
fn get_table_mut_internal<'a>(
&'a mut self,
table_path: &[String],
insert_if_not_exists: bool,
) -> CargoResult<&'a mut toml_edit::Item> {
fn descend<'a>(
input: &'a mut toml_edit::Item,
path: &[String],
insert_if_not_exists: bool,
) -> CargoResult<&'a mut toml_edit::Item> {
if let Some(segment) = path.first() {
let value = if insert_if_not_exists {
input[&segment].or_insert(toml_edit::table())
} else {
input
.get_mut(segment)
.ok_or_else(|| non_existent_table_err(segment))?
};
if value.is_table_like() {
descend(value, &path[1..], insert_if_not_exists)
} else {
Err(non_existent_table_err(segment))
}
} else {
Ok(input)
}
}
descend(self.data.as_item_mut(), table_path, insert_if_not_exists)
}
}
impl str::FromStr for Manifest {
type Err = anyhow::Error;
fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
let d: toml_edit::Document = input.parse().context("Manifest not valid TOML")?;
Ok(Manifest { data: d })
}
}
impl std::fmt::Display for Manifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self.data.to_string();
s.fmt(f)
}
}
#[derive(Debug)]
pub struct LocalManifest {
pub path: PathBuf,
pub manifest: Manifest,
}
impl Deref for LocalManifest {
type Target = Manifest;
fn deref(&self) -> &Manifest {
&self.manifest
}
}
impl DerefMut for LocalManifest {
fn deref_mut(&mut self) -> &mut Manifest {
&mut self.manifest
}
}
impl LocalManifest {
pub fn find(path: Option<&Path>) -> CargoResult<Self> {
let path = dunce::canonicalize(find(path)?)?;
Self::try_new(&path)
}
pub fn try_new(path: &Path) -> CargoResult<Self> {
if !path.is_absolute() {
anyhow::bail!("can only edit absolute paths, got {}", path.display());
}
let data = fs::read_to_string(path).with_context(|| "Failed to read manifest contents")?;
let manifest = data.parse().context("Unable to parse Cargo.toml")?;
Ok(LocalManifest {
manifest,
path: path.to_owned(),
})
}
pub fn write(&self) -> CargoResult<()> {
let s = self.manifest.data.to_string();
let new_contents_bytes = s.as_bytes();
fs::write(&self.path, new_contents_bytes).context("Failed to write updated Cargo.toml")
}
pub fn remove_from_table(&mut self, table_path: &[String], name: &str) -> CargoResult<()> {
let parent_table = self.get_table_mut(table_path)?;
{
let dep = parent_table
.get_mut(name)
.filter(|t| !t.is_none())
.ok_or_else(|| non_existent_dependency_err(name, table_path.join(".")))?;
*dep = toml_edit::Item::None;
}
if parent_table.as_table_like().unwrap().is_empty() {
*parent_table = toml_edit::Item::None;
}
Ok(())
}
pub fn get_dependency_tables_mut(
&mut self,
) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
let root = self.data.as_table_mut();
root.iter_mut().flat_map(|(k, v)| {
if DepTable::KINDS
.iter()
.any(|kind| kind.kind_table() == k.get())
{
v.as_table_like_mut().into_iter().collect::<Vec<_>>()
} else if k == "workspace" {
v.as_table_like_mut()
.unwrap()
.iter_mut()
.filter_map(|(k, v)| {
if k.get() == "dependencies" {
v.as_table_like_mut()
} else {
None
}
})
.collect::<Vec<_>>()
} else if k == "target" {
v.as_table_like_mut()
.unwrap()
.iter_mut()
.flat_map(|(_, v)| {
v.as_table_like_mut().into_iter().flat_map(|v| {
v.iter_mut().filter_map(|(k, v)| {
if DepTable::KINDS
.iter()
.any(|kind| kind.kind_table() == k.get())
{
v.as_table_like_mut()
} else {
None
}
})
})
})
.collect::<Vec<_>>()
} else {
Vec::new()
}
})
}
pub fn get_workspace_dependency_table_mut(&mut self) -> Option<&mut dyn toml_edit::TableLike> {
self.data
.get_mut("workspace")?
.get_mut("dependencies")?
.as_table_like_mut()
}
pub fn set_package_version(&mut self, version: &Version) {
self.data["package"]["version"] = toml_edit::value(version.to_string());
}
pub fn version_is_inherited(&self) -> bool {
fn inherits_workspace_version_impl(this: &Manifest) -> Option<bool> {
this.data
.get("package")?
.get("version")?
.get("workspace")?
.as_bool()
}
inherits_workspace_version_impl(self).unwrap_or(false)
}
pub fn get_workspace_version(&self) -> Option<Version> {
let version = self
.data
.get("workspace")?
.get("package")?
.get("version")?
.as_str()?;
Version::parse(version).ok()
}
pub fn set_workspace_version(&mut self, version: &Version) {
self.data["workspace"]["package"]["version"] = toml_edit::value(version.to_string());
}
pub fn gc_dep(&mut self, dep_key: &str) {
let status = self.dep_feature(dep_key);
if matches!(status, FeatureStatus::None | FeatureStatus::DepFeature) {
if let toml_edit::Item::Table(feature_table) = &mut self.data.as_table_mut()["features"]
{
for (_feature, mut activated_crates) in feature_table.iter_mut() {
if let toml_edit::Item::Value(toml_edit::Value::Array(feature_activations)) =
&mut activated_crates
{
remove_feature_activation(feature_activations, dep_key, status);
}
}
}
}
}
fn dep_feature(&self, dep_key: &str) -> FeatureStatus {
let mut status = FeatureStatus::None;
for (_, tbl) in self.get_sections() {
if let toml_edit::Item::Table(tbl) = tbl {
if let Some(dep_item) = tbl.get(dep_key) {
let optional = dep_item.get("optional");
let optional = optional.and_then(|i| i.as_value());
let optional = optional.and_then(|i| i.as_bool());
let optional = optional.unwrap_or(false);
if optional {
return FeatureStatus::Feature;
} else {
status = FeatureStatus::DepFeature;
}
}
}
}
status
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum FeatureStatus {
None,
DepFeature,
Feature,
}
fn remove_feature_activation(
feature_activations: &mut toml_edit::Array,
dep: &str,
status: FeatureStatus,
) {
let dep_feature: &str = &format!("{dep}/",);
let remove_list: Vec<usize> = feature_activations
.iter()
.enumerate()
.filter_map(|(idx, feature_activation)| {
if let toml_edit::Value::String(feature_activation) = feature_activation {
let activation = feature_activation.value();
#[allow(clippy::unnecessary_lazy_evaluations)] match status {
FeatureStatus::None => activation == dep || activation.starts_with(dep_feature),
FeatureStatus::DepFeature => activation == dep,
FeatureStatus::Feature => false,
}
.then(|| idx)
} else {
None
}
})
.collect();
for idx in remove_list.iter().rev() {
feature_activations.remove(*idx);
}
}
pub fn find(specified: Option<&Path>) -> CargoResult<PathBuf> {
match specified {
Some(path)
if fs::metadata(path)
.with_context(|| "Failed to get cargo file metadata")?
.is_file() =>
{
Ok(path.to_owned())
}
Some(path) => find_manifest_path(path),
None => find_manifest_path(
&env::current_dir().with_context(|| "Failed to get current directory")?,
),
}
}
pub fn get_dep_version(dep_item: &toml_edit::Item) -> CargoResult<&str> {
if let Some(req) = dep_item.as_str() {
Ok(req)
} else if dep_item.is_table_like() {
let version = dep_item
.get("version")
.ok_or_else(|| anyhow::format_err!("Missing version field"))?;
version
.as_str()
.ok_or_else(|| anyhow::format_err!("Expect version to be a string"))
} else {
anyhow::bail!("Invalid dependency type");
}
}
pub fn set_dep_version(dep_item: &mut toml_edit::Item, new_version: &str) -> CargoResult<()> {
if dep_item.is_str() {
overwrite_value(dep_item, new_version);
} else if let Some(table) = dep_item.as_table_like_mut() {
let version = table
.get_mut("version")
.ok_or_else(|| anyhow::format_err!("Missing version field"))?;
overwrite_value(version, new_version);
} else {
anyhow::bail!("Invalid dependency type");
}
Ok(())
}
fn overwrite_value(item: &mut toml_edit::Item, value: impl Into<toml_edit::Value>) {
let mut value = value.into();
let existing_decor = item
.as_value()
.map(|v| v.decor().clone())
.unwrap_or_default();
*value.decor_mut() = existing_decor;
*item = toml_edit::Item::Value(value);
}
pub 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)
}