1use std::fs;
2use std::ops::{Deref, DerefMut};
3use std::path::{Path, PathBuf};
4use std::{env, str};
5
6use semver::Version;
7
8use super::errors::*;
9use super::metadata::find_manifest_path;
10
11#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Debug, Copy)]
12pub enum DepKind {
13 Normal,
14 Development,
15 Build,
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct DepTable {
21 kind: DepKind,
22 target: Option<String>,
23}
24
25impl DepTable {
26 const KINDS: &'static [Self] = &[
27 Self::new().set_kind(DepKind::Normal),
28 Self::new().set_kind(DepKind::Development),
29 Self::new().set_kind(DepKind::Build),
30 ];
31
32 pub(crate) const fn new() -> Self {
34 Self {
35 kind: DepKind::Normal,
36 target: None,
37 }
38 }
39
40 pub(crate) const fn set_kind(mut self, kind: DepKind) -> Self {
42 self.kind = kind;
43 self
44 }
45
46 pub(crate) fn set_target(mut self, target: impl Into<String>) -> Self {
48 self.target = Some(target.into());
49 self
50 }
51
52 fn kind_table(&self) -> &str {
53 match self.kind {
54 DepKind::Normal => "dependencies",
55 DepKind::Development => "dev-dependencies",
56 DepKind::Build => "build-dependencies",
57 }
58 }
59}
60
61impl Default for DepTable {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl From<DepKind> for DepTable {
68 fn from(other: DepKind) -> Self {
69 Self::new().set_kind(other)
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct Manifest {
76 pub data: toml_edit::DocumentMut,
78}
79
80impl Manifest {
81 pub(crate) fn get_table_mut<'a>(
86 &'a mut self,
87 table_path: &[String],
88 ) -> CargoResult<&'a mut toml_edit::Item> {
89 self.get_table_mut_internal(table_path, false)
90 }
91
92 pub(crate) fn get_sections(&self) -> Vec<(DepTable, toml_edit::Item)> {
95 let mut sections = Vec::new();
96
97 for table in DepTable::KINDS {
98 let dependency_type = table.kind_table();
99 if self
101 .data
102 .get(dependency_type)
103 .map(|t| t.is_table_like())
104 .unwrap_or(false)
105 {
106 sections.push((table.clone(), self.data[dependency_type].clone()))
107 }
108
109 let target_sections = self
111 .data
112 .as_table()
113 .get("target")
114 .and_then(toml_edit::Item::as_table_like)
115 .into_iter()
116 .flat_map(toml_edit::TableLike::iter)
117 .filter_map(|(target_name, target_table)| {
118 let dependency_table = target_table.get(dependency_type)?;
119 dependency_table.as_table_like().map(|_| {
120 (
121 table.clone().set_target(target_name),
122 dependency_table.clone(),
123 )
124 })
125 });
126
127 sections.extend(target_sections);
128 }
129
130 sections
131 }
132
133 fn get_table_mut_internal<'a>(
134 &'a mut self,
135 table_path: &[String],
136 insert_if_not_exists: bool,
137 ) -> CargoResult<&'a mut toml_edit::Item> {
138 fn descend<'a>(
140 input: &'a mut toml_edit::Item,
141 path: &[String],
142 insert_if_not_exists: bool,
143 ) -> CargoResult<&'a mut toml_edit::Item> {
144 if let Some(segment) = path.first() {
145 let value = if insert_if_not_exists {
146 input[&segment].or_insert(toml_edit::table())
147 } else {
148 input
149 .get_mut(segment)
150 .ok_or_else(|| non_existent_table_err(segment))?
151 };
152
153 if value.is_table_like() {
154 descend(value, &path[1..], insert_if_not_exists)
155 } else {
156 Err(non_existent_table_err(segment))
157 }
158 } else {
159 Ok(input)
160 }
161 }
162
163 descend(self.data.as_item_mut(), table_path, insert_if_not_exists)
164 }
165}
166
167impl str::FromStr for Manifest {
168 type Err = anyhow::Error;
169
170 fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
172 let d: toml_edit::DocumentMut = input.parse().context("Manifest not valid TOML")?;
173
174 Ok(Manifest { data: d })
175 }
176}
177
178impl std::fmt::Display for Manifest {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 let s = self.data.to_string();
181 s.fmt(f)
182 }
183}
184
185#[derive(Debug)]
187pub struct LocalManifest {
188 pub path: PathBuf,
190 pub manifest: Manifest,
192}
193
194impl Deref for LocalManifest {
195 type Target = Manifest;
196
197 fn deref(&self) -> &Manifest {
198 &self.manifest
199 }
200}
201
202impl DerefMut for LocalManifest {
203 fn deref_mut(&mut self) -> &mut Manifest {
204 &mut self.manifest
205 }
206}
207
208impl LocalManifest {
209 pub fn find(path: Option<&Path>) -> CargoResult<Self> {
212 let path = dunce::canonicalize(find(path)?)?;
213 Self::try_new(&path)
214 }
215
216 pub fn try_new(path: &Path) -> CargoResult<Self> {
218 if !path.is_absolute() {
219 anyhow::bail!("can only edit absolute paths, got {}", path.display());
220 }
221 let data = fs::read_to_string(path).with_context(|| "Failed to read manifest contents")?;
222 let manifest = data.parse().context("Unable to parse Cargo.toml")?;
223 Ok(LocalManifest {
224 manifest,
225 path: path.to_owned(),
226 })
227 }
228
229 pub fn write(&self) -> CargoResult<()> {
231 let s = self.manifest.data.to_string();
232 let new_contents_bytes = s.as_bytes();
233
234 fs::write(&self.path, new_contents_bytes).context("Failed to write updated Cargo.toml")
235 }
236
237 pub fn remove_from_table(&mut self, table_path: &[String], name: &str) -> CargoResult<()> {
257 let parent_table = self.get_table_mut(table_path)?;
258
259 {
260 let dep = parent_table
261 .get_mut(name)
262 .filter(|t| !t.is_none())
263 .ok_or_else(|| non_existent_dependency_err(name, table_path.join(".")))?;
264 *dep = toml_edit::Item::None;
266 }
267
268 if parent_table.as_table_like().unwrap().is_empty() {
270 *parent_table = toml_edit::Item::None;
271 }
272
273 Ok(())
274 }
275
276 pub fn get_dependency_tables_mut(
278 &mut self,
279 ) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
280 let root = self.data.as_table_mut();
281 root.iter_mut().flat_map(|(k, v)| {
282 if DepTable::KINDS
283 .iter()
284 .any(|kind| kind.kind_table() == k.get())
285 {
286 v.as_table_like_mut().into_iter().collect::<Vec<_>>()
287 } else if k == "workspace" {
288 v.as_table_like_mut()
289 .unwrap()
290 .iter_mut()
291 .filter_map(|(k, v)| {
292 if k.get() == "dependencies" {
293 v.as_table_like_mut()
294 } else {
295 None
296 }
297 })
298 .collect::<Vec<_>>()
299 } else if k == "target" {
300 v.as_table_like_mut()
301 .unwrap()
302 .iter_mut()
303 .flat_map(|(_, v)| {
304 v.as_table_like_mut().into_iter().flat_map(|v| {
305 v.iter_mut().filter_map(|(k, v)| {
306 if DepTable::KINDS
307 .iter()
308 .any(|kind| kind.kind_table() == k.get())
309 {
310 v.as_table_like_mut()
311 } else {
312 None
313 }
314 })
315 })
316 })
317 .collect::<Vec<_>>()
318 } else {
319 Vec::new()
320 }
321 })
322 }
323
324 pub fn get_workspace_dependency_table_mut(&mut self) -> Option<&mut dyn toml_edit::TableLike> {
326 self.data
327 .get_mut("workspace")?
328 .get_mut("dependencies")?
329 .as_table_like_mut()
330 }
331
332 pub fn set_package_version(&mut self, version: &Version) {
334 self.data["package"]["version"] = toml_edit::value(version.to_string());
335 }
336
337 pub fn version_is_inherited(&self) -> bool {
339 fn inherits_workspace_version_impl(this: &Manifest) -> Option<bool> {
340 this.data
341 .get("package")?
342 .get("version")?
343 .get("workspace")?
344 .as_bool()
345 }
346
347 inherits_workspace_version_impl(self).unwrap_or(false)
348 }
349
350 pub fn get_workspace_version(&self) -> Option<Version> {
352 let version = self
353 .data
354 .get("workspace")?
355 .get("package")?
356 .get("version")?
357 .as_str()?;
358 Version::parse(version).ok()
359 }
360
361 pub fn set_workspace_version(&mut self, version: &Version) {
363 self.data["workspace"]["package"]["version"] = toml_edit::value(version.to_string());
364 }
365
366 pub fn gc_dep(&mut self, dep_key: &str) {
368 let status = self.dep_feature(dep_key);
369 if matches!(status, FeatureStatus::None | FeatureStatus::DepFeature) {
370 if let toml_edit::Item::Table(feature_table) = &mut self.data.as_table_mut()["features"]
371 {
372 for (_feature, mut activated_crates) in feature_table.iter_mut() {
373 if let toml_edit::Item::Value(toml_edit::Value::Array(feature_activations)) =
374 &mut activated_crates
375 {
376 remove_feature_activation(feature_activations, dep_key, status);
377 }
378 }
379 }
380 }
381 }
382
383 fn dep_feature(&self, dep_key: &str) -> FeatureStatus {
384 let mut status = FeatureStatus::None;
385 for (_, tbl) in self.get_sections() {
386 if let toml_edit::Item::Table(tbl) = tbl {
387 if let Some(dep_item) = tbl.get(dep_key) {
388 let optional = dep_item.get("optional");
389 let optional = optional.and_then(|i| i.as_value());
390 let optional = optional.and_then(|i| i.as_bool());
391 let optional = optional.unwrap_or(false);
392 if optional {
393 return FeatureStatus::Feature;
394 } else {
395 status = FeatureStatus::DepFeature;
396 }
397 }
398 }
399 }
400 status
401 }
402}
403
404#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
405enum FeatureStatus {
406 None,
407 DepFeature,
408 Feature,
409}
410
411fn remove_feature_activation(
412 feature_activations: &mut toml_edit::Array,
413 dep: &str,
414 status: FeatureStatus,
415) {
416 let dep_feature: &str = &format!("{dep}/",);
417
418 let remove_list: Vec<usize> = feature_activations
419 .iter()
420 .enumerate()
421 .filter_map(|(idx, feature_activation)| {
422 if let toml_edit::Value::String(feature_activation) = feature_activation {
423 let activation = feature_activation.value();
424 #[allow(clippy::unnecessary_lazy_evaluations)] match status {
426 FeatureStatus::None => activation == dep || activation.starts_with(dep_feature),
427 FeatureStatus::DepFeature => activation == dep,
428 FeatureStatus::Feature => false,
429 }
430 .then(|| idx)
431 } else {
432 None
433 }
434 })
435 .collect();
436
437 for idx in remove_list.iter().rev() {
439 feature_activations.remove(*idx);
440 }
441}
442
443pub fn find(specified: Option<&Path>) -> CargoResult<PathBuf> {
449 match specified {
450 Some(path)
451 if fs::metadata(path)
452 .with_context(|| "Failed to get cargo file metadata")?
453 .is_file() =>
454 {
455 Ok(path.to_owned())
456 }
457 Some(path) => find_manifest_path(path),
458 None => find_manifest_path(
459 &env::current_dir().with_context(|| "Failed to get current directory")?,
460 ),
461 }
462}
463
464pub fn get_dep_version(dep_item: &toml_edit::Item) -> CargoResult<&str> {
466 if let Some(req) = dep_item.as_str() {
467 Ok(req)
468 } else if dep_item.is_table_like() {
469 let version = dep_item
470 .get("version")
471 .ok_or_else(|| anyhow::format_err!("Missing version field"))?;
472 version
473 .as_str()
474 .ok_or_else(|| anyhow::format_err!("Expect version to be a string"))
475 } else {
476 anyhow::bail!("Invalid dependency type");
477 }
478}
479
480pub fn set_dep_version(dep_item: &mut toml_edit::Item, new_version: &str) -> CargoResult<()> {
482 if dep_item.is_str() {
483 overwrite_value(dep_item, new_version);
484 } else if let Some(table) = dep_item.as_table_like_mut() {
485 let version = table
486 .get_mut("version")
487 .ok_or_else(|| anyhow::format_err!("Missing version field"))?;
488 overwrite_value(version, new_version);
489 } else {
490 anyhow::bail!("Invalid dependency type");
491 }
492 Ok(())
493}
494
495fn overwrite_value(item: &mut toml_edit::Item, value: impl Into<toml_edit::Value>) {
497 let mut value = value.into();
498
499 let existing_decor = item
500 .as_value()
501 .map(|v| v.decor().clone())
502 .unwrap_or_default();
503
504 *value.decor_mut() = existing_decor;
505
506 *item = toml_edit::Item::Value(value);
507}
508
509pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
510 item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
511}