cargo/util/toml_mut/
manifest.rs

1//! Parsing and editing of manifest files.
2
3use std::ops::{Deref, DerefMut};
4use std::path::{Path, PathBuf};
5use std::str;
6
7use anyhow::Context as _;
8
9use super::dependency::Dependency;
10use crate::core::dependency::DepKind;
11use crate::core::{FeatureValue, Features, Workspace};
12use crate::util::closest;
13use crate::util::toml::{is_embedded, ScriptSource};
14use crate::{CargoResult, GlobalContext};
15
16/// Dependency table to add deps to.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct DepTable {
19    kind: DepKind,
20    target: Option<String>,
21}
22
23impl DepTable {
24    const KINDS: &'static [Self] = &[
25        Self::new().set_kind(DepKind::Normal),
26        Self::new().set_kind(DepKind::Development),
27        Self::new().set_kind(DepKind::Build),
28    ];
29
30    /// Reference to a Dependency Table.
31    pub const fn new() -> Self {
32        Self {
33            kind: DepKind::Normal,
34            target: None,
35        }
36    }
37
38    /// Choose the type of dependency.
39    pub const fn set_kind(mut self, kind: DepKind) -> Self {
40        self.kind = kind;
41        self
42    }
43
44    /// Choose the platform for the dependency.
45    pub fn set_target(mut self, target: impl Into<String>) -> Self {
46        self.target = Some(target.into());
47        self
48    }
49
50    /// Type of dependency.
51    pub fn kind(&self) -> DepKind {
52        self.kind
53    }
54
55    /// Platform for the dependency.
56    pub fn target(&self) -> Option<&str> {
57        self.target.as_deref()
58    }
59
60    /// Keys to the table.
61    pub fn to_table(&self) -> Vec<&str> {
62        if let Some(target) = &self.target {
63            vec!["target", target, self.kind.kind_table()]
64        } else {
65            vec![self.kind.kind_table()]
66        }
67    }
68}
69
70impl Default for DepTable {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl From<DepKind> for DepTable {
77    fn from(other: DepKind) -> Self {
78        Self::new().set_kind(other)
79    }
80}
81
82/// An editable Cargo manifest.
83#[derive(Debug, Clone)]
84pub struct Manifest {
85    /// Manifest contents as TOML data.
86    pub data: toml_edit::DocumentMut,
87}
88
89impl Manifest {
90    /// Get the manifest's package name.
91    pub fn package_name(&self) -> CargoResult<&str> {
92        self.data
93            .as_table()
94            .get("package")
95            .and_then(|m| m.get("name"))
96            .and_then(|m| m.as_str())
97            .ok_or_else(parse_manifest_err)
98    }
99
100    /// Get the specified table from the manifest.
101    pub fn get_table<'a>(&'a self, table_path: &[String]) -> CargoResult<&'a toml_edit::Item> {
102        /// Descend into a manifest until the required table is found.
103        fn descend<'a>(
104            input: &'a toml_edit::Item,
105            path: &[String],
106        ) -> CargoResult<&'a toml_edit::Item> {
107            if let Some(segment) = path.get(0) {
108                let value = input
109                    .get(&segment)
110                    .ok_or_else(|| non_existent_table_err(segment))?;
111
112                if value.is_table_like() {
113                    descend(value, &path[1..])
114                } else {
115                    Err(non_existent_table_err(segment))
116                }
117            } else {
118                Ok(input)
119            }
120        }
121
122        descend(self.data.as_item(), table_path)
123    }
124
125    /// Get the specified table from the manifest.
126    pub fn get_table_mut<'a>(
127        &'a mut self,
128        table_path: &[String],
129    ) -> CargoResult<&'a mut toml_edit::Item> {
130        /// Descend into a manifest until the required table is found.
131        fn descend<'a>(
132            input: &'a mut toml_edit::Item,
133            path: &[String],
134        ) -> CargoResult<&'a mut toml_edit::Item> {
135            if let Some(segment) = path.get(0) {
136                let mut default_table = toml_edit::Table::new();
137                default_table.set_implicit(true);
138                let value = input[&segment].or_insert(toml_edit::Item::Table(default_table));
139
140                if value.is_table_like() {
141                    descend(value, &path[1..])
142                } else {
143                    Err(non_existent_table_err(segment))
144                }
145            } else {
146                Ok(input)
147            }
148        }
149
150        descend(self.data.as_item_mut(), table_path)
151    }
152
153    /// Get all sections in the manifest that exist and might contain
154    /// dependencies. The returned items are always `Table` or
155    /// `InlineTable`.
156    pub fn get_sections(&self) -> Vec<(DepTable, toml_edit::Item)> {
157        let mut sections = Vec::new();
158
159        for table in DepTable::KINDS {
160            let dependency_type = table.kind.kind_table();
161            // Dependencies can be in the three standard sections...
162            if self
163                .data
164                .get(dependency_type)
165                .map(|t| t.is_table_like())
166                .unwrap_or(false)
167            {
168                sections.push((table.clone(), self.data[dependency_type].clone()))
169            }
170
171            // ... and in `target.<target>.(build-/dev-)dependencies`.
172            let target_sections = self
173                .data
174                .as_table()
175                .get("target")
176                .and_then(toml_edit::Item::as_table_like)
177                .into_iter()
178                .flat_map(toml_edit::TableLike::iter)
179                .filter_map(|(target_name, target_table)| {
180                    let dependency_table = target_table.get(dependency_type)?;
181                    dependency_table.as_table_like().map(|_| {
182                        (
183                            table.clone().set_target(target_name),
184                            dependency_table.clone(),
185                        )
186                    })
187                });
188
189            sections.extend(target_sections);
190        }
191
192        sections
193    }
194
195    pub fn get_legacy_sections(&self) -> Vec<String> {
196        let mut result = Vec::new();
197
198        for dependency_type in ["dev_dependencies", "build_dependencies"] {
199            if self.data.contains_key(dependency_type) {
200                result.push(dependency_type.to_owned());
201            }
202
203            // ... and in `target.<target>.(build-/dev-)dependencies`.
204            result.extend(
205                self.data
206                    .as_table()
207                    .get("target")
208                    .and_then(toml_edit::Item::as_table_like)
209                    .into_iter()
210                    .flat_map(toml_edit::TableLike::iter)
211                    .filter_map(|(target_name, target_table)| {
212                        if target_table.as_table_like()?.contains_key(dependency_type) {
213                            Some(format!("target.{target_name}.{dependency_type}"))
214                        } else {
215                            None
216                        }
217                    }),
218            );
219        }
220        result
221    }
222}
223
224impl str::FromStr for Manifest {
225    type Err = anyhow::Error;
226
227    /// Read manifest data from string
228    fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
229        let d: toml_edit::DocumentMut = input.parse().context("Manifest not valid TOML")?;
230
231        Ok(Manifest { data: d })
232    }
233}
234
235impl std::fmt::Display for Manifest {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        self.data.fmt(f)
238    }
239}
240
241/// An editable Cargo manifest that is available locally.
242#[derive(Debug, Clone)]
243pub struct LocalManifest {
244    /// Path to the manifest.
245    pub path: PathBuf,
246    /// Manifest contents.
247    pub manifest: Manifest,
248    /// The raw, unparsed package file
249    pub raw: String,
250    /// Edit location for an embedded manifest, if relevant
251    pub embedded: Option<Embedded>,
252}
253
254impl Deref for LocalManifest {
255    type Target = Manifest;
256
257    fn deref(&self) -> &Manifest {
258        &self.manifest
259    }
260}
261
262impl DerefMut for LocalManifest {
263    fn deref_mut(&mut self) -> &mut Manifest {
264        &mut self.manifest
265    }
266}
267
268impl LocalManifest {
269    /// Construct the `LocalManifest` corresponding to the `Path` provided..
270    pub fn try_new(path: &Path) -> CargoResult<Self> {
271        if !path.is_absolute() {
272            anyhow::bail!("can only edit absolute paths, got {}", path.display());
273        }
274        let raw = cargo_util::paths::read(&path)?;
275        let mut data = raw.clone();
276        let mut embedded = None;
277        if is_embedded(path) {
278            let source = ScriptSource::parse(&data)?;
279            if let Some(frontmatter) = source.frontmatter() {
280                embedded = Some(Embedded::exists(&data, frontmatter));
281                data = frontmatter.to_owned();
282            } else if let Some(shebang) = source.shebang() {
283                embedded = Some(Embedded::after(&data, shebang));
284                data = String::new();
285            } else {
286                embedded = Some(Embedded::start());
287                data = String::new();
288            }
289        }
290        let manifest = data.parse().context("Unable to parse Cargo.toml")?;
291        Ok(LocalManifest {
292            manifest,
293            path: path.to_owned(),
294            raw,
295            embedded,
296        })
297    }
298
299    /// Write changes back to the file.
300    pub fn write(&self) -> CargoResult<()> {
301        let mut manifest = self.manifest.data.to_string();
302        let raw = match self.embedded.as_ref() {
303            Some(Embedded::Implicit(start)) => {
304                if !manifest.ends_with("\n") {
305                    manifest.push_str("\n");
306                }
307                let fence = "---\n";
308                let prefix = &self.raw[0..*start];
309                let suffix = &self.raw[*start..];
310                let empty_line = if prefix.is_empty() { "\n" } else { "" };
311                format!("{prefix}{fence}{manifest}{fence}{empty_line}{suffix}")
312            }
313            Some(Embedded::Explicit(span)) => {
314                if !manifest.ends_with("\n") {
315                    manifest.push_str("\n");
316                }
317                let prefix = &self.raw[0..span.start];
318                let suffix = &self.raw[span.end..];
319                format!("{prefix}{manifest}{suffix}")
320            }
321            None => manifest,
322        };
323        let new_contents_bytes = raw.as_bytes();
324
325        cargo_util::paths::write_atomic(&self.path, new_contents_bytes)
326    }
327
328    /// Lookup a dependency.
329    pub fn get_dependency_versions<'s>(
330        &'s self,
331        dep_key: &'s str,
332        ws: &'s Workspace<'_>,
333        unstable_features: &'s Features,
334    ) -> impl Iterator<Item = (DepTable, CargoResult<Dependency>)> + 's {
335        let crate_root = self.path.parent().expect("manifest path is absolute");
336        self.get_sections()
337            .into_iter()
338            .filter_map(move |(table_path, table)| {
339                let table = table.into_table().ok()?;
340                Some(
341                    table
342                        .into_iter()
343                        .filter_map(|(key, item)| {
344                            if key.as_str() == dep_key {
345                                Some((table_path.clone(), key, item))
346                            } else {
347                                None
348                            }
349                        })
350                        .collect::<Vec<_>>(),
351                )
352            })
353            .flatten()
354            .map(move |(table_path, dep_key, dep_item)| {
355                let dep = Dependency::from_toml(
356                    ws.gctx(),
357                    ws.root(),
358                    crate_root,
359                    unstable_features,
360                    &dep_key,
361                    &dep_item,
362                );
363                (table_path, dep)
364            })
365    }
366
367    /// Add entry to a Cargo.toml.
368    pub fn insert_into_table(
369        &mut self,
370        table_path: &[String],
371        dep: &Dependency,
372        gctx: &GlobalContext,
373        workspace_root: &Path,
374        unstable_features: &Features,
375    ) -> CargoResult<()> {
376        let crate_root = self
377            .path
378            .parent()
379            .expect("manifest path is absolute")
380            .to_owned();
381        let dep_key = dep.toml_key();
382
383        let table = self.get_table_mut(table_path)?;
384        if let Some((mut dep_key, dep_item)) = table
385            .as_table_like_mut()
386            .unwrap()
387            .get_key_value_mut(dep_key)
388        {
389            dep.update_toml(
390                gctx,
391                workspace_root,
392                &crate_root,
393                unstable_features,
394                &mut dep_key,
395                dep_item,
396            )?;
397            if let Some(table) = dep_item.as_inline_table_mut() {
398                // So long as we don't have `Cargo.toml` auto-formatting and inline-tables can only
399                // be on one line, there isn't really much in the way of interesting formatting to
400                // include (no comments), so let's just wipe it clean
401                table.fmt();
402            }
403        } else {
404            let new_dependency =
405                dep.to_toml(gctx, workspace_root, &crate_root, unstable_features)?;
406            table[dep_key] = new_dependency;
407        }
408
409        Ok(())
410    }
411
412    /// Remove entry from a Cargo.toml.
413    pub fn remove_from_table(&mut self, table_path: &[String], name: &str) -> CargoResult<()> {
414        let parent_table = self.get_table_mut(table_path)?;
415
416        match parent_table.get_mut(name).filter(|t| !t.is_none()) {
417            Some(dep) => {
418                // remove the dependency
419                *dep = toml_edit::Item::None;
420
421                // remove table if empty
422                if parent_table.as_table_like().unwrap().is_empty() {
423                    *parent_table = toml_edit::Item::None;
424                }
425            }
426            None => {
427                let names = parent_table
428                    .as_table_like()
429                    .map(|t| t.iter())
430                    .into_iter()
431                    .flatten();
432                let alt_name = closest(name, names.map(|(k, _)| k), |k| k).map(|n| n.to_owned());
433
434                // Search in other tables.
435                let sections = self.get_sections();
436                let found_table_path = sections.iter().find_map(|(t, i)| {
437                    let table_path: Vec<String> =
438                        t.to_table().iter().map(|s| s.to_string()).collect();
439                    i.get(name).is_some().then(|| table_path.join("."))
440                });
441
442                return Err(non_existent_dependency_err(
443                    name,
444                    table_path.join("."),
445                    found_table_path,
446                    alt_name.as_deref(),
447                ));
448            }
449        }
450
451        Ok(())
452    }
453
454    /// Allow mutating dependencies, wherever they live.
455    /// Copied from cargo-edit.
456    pub fn get_dependency_tables_mut(
457        &mut self,
458    ) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
459        let root = self.data.as_table_mut();
460        root.iter_mut().flat_map(|(k, v)| {
461            if DepTable::KINDS
462                .iter()
463                .any(|dt| dt.kind.kind_table() == k.get())
464            {
465                v.as_table_like_mut().into_iter().collect::<Vec<_>>()
466            } else if k == "workspace" {
467                v.as_table_like_mut()
468                    .unwrap()
469                    .iter_mut()
470                    .filter_map(|(k, v)| {
471                        if k.get() == "dependencies" {
472                            v.as_table_like_mut()
473                        } else {
474                            None
475                        }
476                    })
477                    .collect::<Vec<_>>()
478            } else if k == "target" {
479                v.as_table_like_mut()
480                    .unwrap()
481                    .iter_mut()
482                    .flat_map(|(_, v)| {
483                        v.as_table_like_mut().into_iter().flat_map(|v| {
484                            v.iter_mut().filter_map(|(k, v)| {
485                                if DepTable::KINDS
486                                    .iter()
487                                    .any(|dt| dt.kind.kind_table() == k.get())
488                                {
489                                    v.as_table_like_mut()
490                                } else {
491                                    None
492                                }
493                            })
494                        })
495                    })
496                    .collect::<Vec<_>>()
497            } else {
498                Vec::new()
499            }
500        })
501    }
502
503    /// Remove references to `dep_key` if its no longer present.
504    pub fn gc_dep(&mut self, dep_key: &str) {
505        let explicit_dep_activation = self.is_explicit_dep_activation(dep_key);
506        let status = self.dep_status(dep_key);
507
508        if let Some(toml_edit::Item::Table(feature_table)) =
509            self.data.as_table_mut().get_mut("features")
510        {
511            for (_feature, mut feature_values) in feature_table.iter_mut() {
512                if let toml_edit::Item::Value(toml_edit::Value::Array(feature_values)) =
513                    &mut feature_values
514                {
515                    fix_feature_activations(
516                        feature_values,
517                        dep_key,
518                        status,
519                        explicit_dep_activation,
520                    );
521                }
522            }
523        }
524    }
525
526    pub fn is_explicit_dep_activation(&self, dep_key: &str) -> bool {
527        if let Some(toml_edit::Item::Table(feature_table)) = self.data.as_table().get("features") {
528            for values in feature_table
529                .iter()
530                .map(|(_, a)| a)
531                .filter_map(|i| i.as_value())
532                .filter_map(|v| v.as_array())
533            {
534                for value in values.iter().filter_map(|v| v.as_str()) {
535                    let value = FeatureValue::new(value.into());
536                    if let FeatureValue::Dep { dep_name } = &value {
537                        if dep_name.as_str() == dep_key {
538                            return true;
539                        }
540                    }
541                }
542            }
543        }
544
545        false
546    }
547
548    fn dep_status(&self, dep_key: &str) -> DependencyStatus {
549        let mut status = DependencyStatus::None;
550        for (_, tbl) in self.get_sections() {
551            if let toml_edit::Item::Table(tbl) = tbl {
552                if let Some(dep_item) = tbl.get(dep_key) {
553                    let optional = dep_item
554                        .get("optional")
555                        .and_then(|i| i.as_value())
556                        .and_then(|i| i.as_bool())
557                        .unwrap_or(false);
558                    if optional {
559                        return DependencyStatus::Optional;
560                    } else {
561                        status = DependencyStatus::Required;
562                    }
563                }
564            }
565        }
566        status
567    }
568}
569
570impl std::fmt::Display for LocalManifest {
571    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572        self.manifest.fmt(f)
573    }
574}
575
576/// Edit location for an embedded manifest
577#[derive(Clone, Debug)]
578pub enum Embedded {
579    /// Manifest is implicit
580    ///
581    /// This is the insert location for a frontmatter
582    Implicit(usize),
583    /// Manifest is explicit in a frontmatter
584    ///
585    /// This is the span of the frontmatter body
586    Explicit(std::ops::Range<usize>),
587}
588
589impl Embedded {
590    fn start() -> Self {
591        Self::Implicit(0)
592    }
593
594    fn after(input: &str, after: &str) -> Self {
595        let span = substr_span(input, after);
596        let end = span.end;
597        Self::Implicit(end)
598    }
599
600    fn exists(input: &str, exists: &str) -> Self {
601        let span = substr_span(input, exists);
602        Self::Explicit(span)
603    }
604}
605
606fn substr_span(haystack: &str, needle: &str) -> std::ops::Range<usize> {
607    let haystack_start_ptr = haystack.as_ptr();
608    let haystack_end_ptr = haystack[haystack.len()..haystack.len()].as_ptr();
609
610    let needle_start_ptr = needle.as_ptr();
611    let needle_end_ptr = needle[needle.len()..needle.len()].as_ptr();
612
613    assert!(needle_end_ptr < haystack_end_ptr);
614    assert!(haystack_start_ptr <= needle_start_ptr);
615    let start = needle_start_ptr as usize - haystack_start_ptr as usize;
616    let end = start + needle.len();
617
618    start..end
619}
620
621#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
622enum DependencyStatus {
623    None,
624    Optional,
625    Required,
626}
627
628fn fix_feature_activations(
629    feature_values: &mut toml_edit::Array,
630    dep_key: &str,
631    status: DependencyStatus,
632    explicit_dep_activation: bool,
633) {
634    let remove_list: Vec<usize> = feature_values
635        .iter()
636        .enumerate()
637        .filter_map(|(idx, value)| value.as_str().map(|s| (idx, s)))
638        .filter_map(|(idx, value)| {
639            let parsed_value = FeatureValue::new(value.into());
640            match status {
641                DependencyStatus::None => match (parsed_value, explicit_dep_activation) {
642                    (FeatureValue::Feature(dep_name), false)
643                    | (FeatureValue::Dep { dep_name }, _)
644                    | (FeatureValue::DepFeature { dep_name, .. }, _) => dep_name == dep_key,
645                    _ => false,
646                },
647                DependencyStatus::Optional => false,
648                DependencyStatus::Required => match (parsed_value, explicit_dep_activation) {
649                    (FeatureValue::Feature(dep_name), false)
650                    | (FeatureValue::Dep { dep_name }, _) => dep_name == dep_key,
651                    (FeatureValue::Feature(_), true) | (FeatureValue::DepFeature { .. }, _) => {
652                        false
653                    }
654                },
655            }
656            .then(|| idx)
657        })
658        .collect();
659
660    // Remove found idx in revers order so we don't invalidate the idx.
661    for idx in remove_list.iter().rev() {
662        remove_array_index(feature_values, *idx);
663    }
664
665    if status == DependencyStatus::Required {
666        for value in feature_values.iter_mut() {
667            let parsed_value = if let Some(value) = value.as_str() {
668                FeatureValue::new(value.into())
669            } else {
670                continue;
671            };
672            if let FeatureValue::DepFeature {
673                dep_name,
674                dep_feature,
675                weak,
676            } = parsed_value
677            {
678                if dep_name == dep_key && weak {
679                    let mut new_value = toml_edit::Value::from(format!("{dep_name}/{dep_feature}"));
680                    *new_value.decor_mut() = value.decor().clone();
681                    *value = new_value;
682                }
683            }
684        }
685    }
686}
687
688pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
689    item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
690}
691
692fn parse_manifest_err() -> anyhow::Error {
693    anyhow::format_err!("unable to parse external Cargo.toml")
694}
695
696fn non_existent_table_err(table: impl std::fmt::Display) -> anyhow::Error {
697    anyhow::format_err!("the table `{table}` could not be found.")
698}
699
700fn non_existent_dependency_err(
701    name: impl std::fmt::Display,
702    search_table: impl std::fmt::Display,
703    found_table: Option<impl std::fmt::Display>,
704    alt_name: Option<&str>,
705) -> anyhow::Error {
706    let mut msg = format!("the dependency `{name}` could not be found in `{search_table}`");
707    if let Some(found_table) = found_table {
708        msg.push_str(&format!("; it is present in `{found_table}`",));
709    } else if let Some(alt_name) = alt_name {
710        msg.push_str(&format!("; dependency `{alt_name}` exists",));
711    }
712    anyhow::format_err!(msg)
713}
714
715fn remove_array_index(array: &mut toml_edit::Array, index: usize) {
716    let value = array.remove(index);
717
718    // Captures all lines before leading whitespace
719    let prefix_lines = value
720        .decor()
721        .prefix()
722        .and_then(|p| p.as_str().expect("spans removed").rsplit_once('\n'))
723        .map(|(lines, _current)| lines);
724    // Captures all lines after trailing whitespace, before the next comma
725    let suffix_lines = value
726        .decor()
727        .suffix()
728        .and_then(|p| p.as_str().expect("spans removed").split_once('\n'))
729        .map(|(_current, lines)| lines);
730    let mut merged_lines = String::new();
731    if let Some(prefix_lines) = prefix_lines {
732        merged_lines.push_str(prefix_lines);
733        merged_lines.push('\n');
734    }
735    if let Some(suffix_lines) = suffix_lines {
736        merged_lines.push_str(suffix_lines);
737        merged_lines.push('\n');
738    }
739
740    let next_index = index; // Since `index` was removed, that effectively auto-advances us
741    if let Some(next) = array.get_mut(next_index) {
742        let next_decor = next.decor_mut();
743        let next_prefix = next_decor
744            .prefix()
745            .map(|s| s.as_str().expect("spans removed"))
746            .unwrap_or_default();
747        merged_lines.push_str(next_prefix);
748        next_decor.set_prefix(merged_lines);
749    } else {
750        let trailing = array.trailing().as_str().expect("spans removed");
751        merged_lines.push_str(trailing);
752        array.set_trailing(merged_lines);
753    }
754}