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