#
cargo/util/toml/
embedded.rs

    1use anyhow::Context as _;
    2
    3use cargo_util_schemas::manifest::PackageName;
    4
    5use crate::util::restricted_names;
    6use crate::CargoResult;
    7use crate::GlobalContext;
    8
    9pub(super) fn expand_manifest(
    10    content: &str,
    11    path: &std::path::Path,
    12    gctx: &GlobalContext,
    13) -> CargoResult<String> {
    14    let source = ScriptSource::parse(content)?;
    15    if let Some(frontmatter) = source.frontmatter() {
    16        match source.info() {
    17            Some("cargo") | None => {}
    18            Some(other) => {
    19                if let Some(remainder) = other.strip_prefix("cargo,") {
    20                    anyhow::bail!("cargo does not support frontmatter infostring attributes like `{remainder}` at this time")
    21                } else {
    22                    anyhow::bail!("frontmatter infostring `{other}` is unsupported by cargo; specify `cargo` for embedding a manifest")
    23                }
    24            }
    25        }
    26
    27        // HACK: until rustc has native support for this syntax, we have to remove it from the
    28        // source file
    29        use std::fmt::Write as _;
    30        let hash = crate::util::hex::short_hash(&path.to_string_lossy());
    31        let mut rel_path = std::path::PathBuf::new();
    32        rel_path.push("target");
    33        rel_path.push(&hash[0..2]);
    34        rel_path.push(&hash[2..]);
    35        let target_dir = gctx.home().join(rel_path);
    36        let hacked_path = target_dir
    37            .join(
    38                path.file_name()
    39                    .expect("always a name for embedded manifests"),
    40            )
    41            .into_path_unlocked();
    42        let mut hacked_source = String::new();
    43        if let Some(shebang) = source.shebang() {
    44            writeln!(hacked_source, "{shebang}")?;
    45        }
    46        writeln!(hacked_source)?; // open
    47        for _ in 0..frontmatter.lines().count() {
    48            writeln!(hacked_source)?;
    49        }
    50        writeln!(hacked_source)?; // close
    51        writeln!(hacked_source, "{}", source.content())?;
    52        if let Some(parent) = hacked_path.parent() {
    53            cargo_util::paths::create_dir_all(parent)?;
    54        }
    55        cargo_util::paths::write_if_changed(&hacked_path, hacked_source)?;
    56
    57        let manifest = inject_bin_path(&frontmatter, &hacked_path)
    58            .with_context(|| format!("failed to parse manifest at `{}`", path.display()))?;
    59        let manifest = toml::to_string_pretty(&manifest)?;
    60        Ok(manifest)
    61    } else {
    62        let frontmatter = "";
    63        let manifest = inject_bin_path(frontmatter, path)
    64            .with_context(|| format!("failed to parse manifest at `{}`", path.display()))?;
    65        let manifest = toml::to_string_pretty(&manifest)?;
    66        Ok(manifest)
    67    }
    68}
    69
    70/// HACK: Add a `[[bin]]` table to the `original_toml`
    71fn inject_bin_path(manifest: &str, path: &std::path::Path) -> CargoResult<toml::Table> {
    72    let mut manifest: toml::Table = toml::from_str(&manifest)?;
    73
    74    let bin_path = path.to_string_lossy().into_owned();
    75    let file_stem = path
    76        .file_stem()
    77        .ok_or_else(|| anyhow::format_err!("no file name"))?
    78        .to_string_lossy();
    79    let name = sanitize_name(file_stem.as_ref());
    80    let bin_name = name.clone();
    81
    82    let mut bin = toml::Table::new();
    83    bin.insert("name".to_owned(), toml::Value::String(bin_name));
    84    bin.insert("path".to_owned(), toml::Value::String(bin_path));
    85    manifest
    86        .entry("bin")
    87        .or_insert_with(|| Vec::<toml::Value>::new().into())
    88        .as_array_mut()
    89        .ok_or_else(|| anyhow::format_err!("`bin` must be an array"))?
    90        .push(toml::Value::Table(bin));
    91
    92    Ok(manifest)
    93}
    94
    95/// Ensure the package name matches the validation from `ops::cargo_new::check_name`
    96pub fn sanitize_name(name: &str) -> String {
    97    let placeholder = if name.contains('_') {
    98        '_'
    99    } else {
    100        // Since embedded manifests only support `[[bin]]`s, prefer arrow-case as that is the
    101        // more common convention for CLIs
    102        '-'
    103    };
    104
    105    let mut name = PackageName::sanitize(name, placeholder).into_inner();
    106
    107    loop {
    108        if restricted_names::is_keyword(&name) {
    109            name.push(placeholder);
    110        } else if restricted_names::is_conflicting_artifact_name(&name) {
    111            // Being an embedded manifest, we always assume it is a `[[bin]]`
    112            name.push(placeholder);
    113        } else if name == "test" {
    114            name.push(placeholder);
    115        } else if restricted_names::is_windows_reserved(&name) {
    116            // Go ahead and be consistent across platforms
    117            name.push(placeholder);
    118        } else {
    119            break;
    120        }
    121    }
    122
    123    name
    124}
    125
    126#[derive(Debug)]
    127pub struct ScriptSource<'s> {
    128    shebang: Option<&'s str>,
    129    info: Option<&'s str>,
    130    frontmatter: Option<&'s str>,
    131    content: &'s str,
    132}
    133
    134impl<'s> ScriptSource<'s> {
    135    pub fn parse(input: &'s str) -> CargoResult<Self> {
    136        let mut source = Self {
    137            shebang: None,
    138            info: None,
    139            frontmatter: None,
    140            content: input,
    141        };
    142
    143        if let Some(shebang_end) = strip_shebang(source.content) {
    144            let (shebang, content) = source.content.split_at(shebang_end);
    145            source.shebang = Some(shebang);
    146            source.content = content;
    147        }
    148
    149        const FENCE_CHAR: char = '-';
    150
    151        let mut rest = source.content;
    152        while !rest.is_empty() {
    153            let without_spaces = rest.trim_start_matches([' ', '\t']);
    154            let without_nl = without_spaces.trim_start_matches(['\r', '\n']);
    155            if without_nl == rest {
    156                // nothing trimmed
    157                break;
    158            } else if without_nl == without_spaces {
    159                // frontmatter must come after a newline
    160                return Ok(source);
    161            }
    162            rest = without_nl;
    163        }
    164        let fence_end = rest
    165            .char_indices()
    166            .find_map(|(i, c)| (c != FENCE_CHAR).then_some(i))
    167            .unwrap_or(source.content.len());
    168        let (fence_pattern, rest) = match fence_end {
    169            0 => {
    170                return Ok(source);
    171            }
    172            1 | 2 => {
    173                anyhow::bail!(
    174                    "found {fence_end} `{FENCE_CHAR}` in rust frontmatter, expected at least 3"
    175                )
    176            }
    177            _ => rest.split_at(fence_end),
    178        };
    179        let nl_fence_pattern = format!("\n{fence_pattern}");
    180        let (info, content) = rest.split_once("\n").unwrap_or((rest, ""));
    181        let info = info.trim();
    182        if !info.is_empty() {
    183            source.info = Some(info);
    184        }
    185        source.content = content;
    186
    187        let Some(frontmatter_nl) = source.content.find(&nl_fence_pattern) else {
    188            anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
    189        };
    190        source.frontmatter = Some(&source.content[..frontmatter_nl + 1]);
    191        source.content = &source.content[frontmatter_nl + nl_fence_pattern.len()..];
    192
    193        let (line, content) = source
    194            .content
    195            .split_once("\n")
    196            .unwrap_or((source.content, ""));
    197        let line = line.trim();
    198        if !line.is_empty() {
    199            anyhow::bail!("unexpected trailing content on closing fence: `{line}`");
    200        }
    201        source.content = content;
    202
    203        Ok(source)
    204    }
    205
    206    pub fn shebang(&self) -> Option<&'s str> {
    207        self.shebang
    208    }
    209
    210    pub fn info(&self) -> Option<&'s str> {
    211        self.info
    212    }
    213
    214    pub fn frontmatter(&self) -> Option<&'s str> {
    215        self.frontmatter
    216    }
    217
    218    pub fn content(&self) -> &'s str {
    219        self.content
    220    }
    221}
    222
    223fn strip_shebang(input: &str) -> Option<usize> {
    224    // See rust-lang/rust's compiler/rustc_lexer/src/lib.rs's `strip_shebang`
    225    // Shebang must start with `#!` literally, without any preceding whitespace.
    226    // For simplicity we consider any line starting with `#!` a shebang,
    227    // regardless of restrictions put on shebangs by specific platforms.
    228    if let Some(rest) = input.strip_prefix("#!") {
    229        // Ok, this is a shebang but if the next non-whitespace token is `[`,
    230        // then it may be valid Rust code, so consider it Rust code.
    231        //
    232        // NOTE: rustc considers line and block comments to be whitespace but to avoid
    233        // any more awareness of Rust grammar, we are excluding it.
    234        if !rest.trim_start().starts_with('[') {
    235            // No other choice than to consider this a shebang.
    236            let newline_end = input.find('\n').map(|pos| pos + 1).unwrap_or(input.len());
    237            return Some(newline_end);
    238        }
    239    }
    240    None
    241}
    242
    243#[cfg(test)]
    244mod test_expand {
    245    use snapbox::assert_data_eq;
    246    use snapbox::prelude::*;
    247    use snapbox::str;
    248
    249    use super::*;
    250
    251    #[track_caller]
    252    fn assert_source(source: &str, expected: impl IntoData) {
    253        use std::fmt::Write as _;
    254
    255        let actual = match ScriptSource::parse(source) {
    256            Ok(actual) => actual,
    257            Err(err) => panic!("unexpected err: {err}"),
    258        };
    259
    260        let mut rendered = String::new();
    261        write_optional_field(&mut rendered, "shebang", actual.shebang());
    262        write_optional_field(&mut rendered, "info", actual.info());
    263        write_optional_field(&mut rendered, "frontmatter", actual.frontmatter());
    264        writeln!(&mut rendered, "content: {:?}", actual.content()).unwrap();
    265        assert_data_eq!(rendered, expected.raw());
    266    }
    267
    268    fn write_optional_field(writer: &mut dyn std::fmt::Write, field: &str, value: Option<&str>) {
    269        if let Some(value) = value {
    270            writeln!(writer, "{field}: {value:?}").unwrap();
    271        } else {
    272            writeln!(writer, "{field}: None").unwrap();
    273        }
    274    }
    275
    276    #[track_caller]
    277    fn assert_err(
    278        result: Result<impl std::fmt::Debug, impl std::fmt::Display>,
    279        err: impl IntoData,
    280    ) {
    281        match result {
    282            Ok(d) => panic!("unexpected Ok({d:#?})"),
    283            Err(actual) => snapbox::assert_data_eq!(actual.to_string(), err.raw()),
    284        }
    285    }
    286
    287    #[test]
    288    fn split_default() {
    289        assert_source(
    290            r#"fn main() {}
    291"#,
    292            str![[r#"
    293shebang: None
    294info: None
    295frontmatter: None
    296content: "fn main() {}\n"
    297
    298"#]],
    299        );
    300    }
    301
    302    #[test]
    303    fn split_dependencies() {
    304        assert_source(
    305            r#"---
    306[dependencies]
    307time="0.1.25"
    308---
    309fn main() {}
    310"#,
    311            str![[r#"
    312shebang: None
    313info: None
    314frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    315content: "fn main() {}\n"
    316
    317"#]],
    318        );
    319    }
    320
    321    #[test]
    322    fn split_infostring() {
    323        assert_source(
    324            r#"---cargo
    325[dependencies]
    326time="0.1.25"
    327---
    328fn main() {}
    329"#,
    330            str![[r#"
    331shebang: None
    332info: "cargo"
    333frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    334content: "fn main() {}\n"
    335
    336"#]],
    337        );
    338    }
    339
    340    #[test]
    341    fn split_infostring_whitespace() {
    342        assert_source(
    343            r#"--- cargo
    344[dependencies]
    345time="0.1.25"
    346---
    347fn main() {}
    348"#,
    349            str![[r#"
    350shebang: None
    351info: "cargo"
    352frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    353content: "fn main() {}\n"
    354
    355"#]],
    356        );
    357    }
    358
    359    #[test]
    360    fn split_shebang() {
    361        assert_source(
    362            r#"#!/usr/bin/env cargo
    363---
    364[dependencies]
    365time="0.1.25"
    366---
    367fn main() {}
    368"#,
    369            str![[r##"
    370shebang: "#!/usr/bin/env cargo\n"
    371info: None
    372frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    373content: "fn main() {}\n"
    374
    375"##]],
    376        );
    377    }
    378
    379    #[test]
    380    fn split_crlf() {
    381        assert_source(
    382                "#!/usr/bin/env cargo\r\n---\r\n[dependencies]\r\ntime=\"0.1.25\"\r\n---\r\nfn main() {}",
    383            str![[r##"
    384shebang: "#!/usr/bin/env cargo\r\n"
    385info: None
    386frontmatter: "[dependencies]\r\ntime=\"0.1.25\"\r\n"
    387content: "fn main() {}"
    388
    389"##]]
    390        );
    391    }
    392
    393    #[test]
    394    fn split_leading_newlines() {
    395        assert_source(
    396            r#"#!/usr/bin/env cargo
    397
    398
    399
    400---
    401[dependencies]
    402time="0.1.25"
    403---
    404
    405
    406fn main() {}
    407"#,
    408            str![[r##"
    409shebang: "#!/usr/bin/env cargo\n"
    410info: None
    411frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    412content: "\n\nfn main() {}\n"
    413
    414"##]],
    415        );
    416    }
    417
    418    #[test]
    419    fn split_attribute() {
    420        assert_source(
    421            r#"#[allow(dead_code)]
    422---
    423[dependencies]
    424time="0.1.25"
    425---
    426fn main() {}
    427"#,
    428            str![[r##"
    429shebang: None
    430info: None
    431frontmatter: None
    432content: "#[allow(dead_code)]\n---\n[dependencies]\ntime=\"0.1.25\"\n---\nfn main() {}\n"
    433
    434"##]],
    435        );
    436    }
    437
    438    #[test]
    439    fn split_extra_dash() {
    440        assert_source(
    441            r#"#!/usr/bin/env cargo
    442----------
    443[dependencies]
    444time="0.1.25"
    445----------
    446
    447fn main() {}"#,
    448            str![[r##"
    449shebang: "#!/usr/bin/env cargo\n"
    450info: None
    451frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    452content: "\nfn main() {}"
    453
    454"##]],
    455        );
    456    }
    457
    458    #[test]
    459    fn split_too_few_dashes() {
    460        assert_err(
    461            ScriptSource::parse(
    462                r#"#!/usr/bin/env cargo
    463--
    464[dependencies]
    465time="0.1.25"
    466--
    467fn main() {}
    468"#,
    469            ),
    470            str!["found 2 `-` in rust frontmatter, expected at least 3"],
    471        );
    472    }
    473
    474    #[test]
    475    fn split_indent() {
    476        assert_source(
    477            r#"#!/usr/bin/env cargo
    478    ---
    479    [dependencies]
    480    time="0.1.25"
    481    ----
    482
    483fn main() {}
    484"#,
    485            str![[r##"
    486shebang: "#!/usr/bin/env cargo\n"
    487info: None
    488frontmatter: None
    489content: "    ---\n    [dependencies]\n    time=\"0.1.25\"\n    ----\n\nfn main() {}\n"
    490
    491"##]],
    492        );
    493    }
    494
    495    #[test]
    496    fn split_escaped() {
    497        assert_source(
    498            r#"#!/usr/bin/env cargo
    499-----
    500---
    501---
    502-----
    503
    504fn main() {}
    505"#,
    506            str![[r##"
    507shebang: "#!/usr/bin/env cargo\n"
    508info: None
    509frontmatter: "---\n---\n"
    510content: "\nfn main() {}\n"
    511
    512"##]],
    513        );
    514    }
    515
    516    #[test]
    517    fn split_invalid_escaped() {
    518        assert_err(
    519            ScriptSource::parse(
    520                r#"#!/usr/bin/env cargo
    521---
    522-----
    523-----
    524---
    525
    526fn main() {}
    527"#,
    528            ),
    529            str!["unexpected trailing content on closing fence: `--`"],
    530        );
    531    }
    532
    533    #[test]
    534    fn split_dashes_in_body() {
    535        assert_source(
    536            r#"#!/usr/bin/env cargo
    537---
    538Hello---
    539World
    540---
    541
    542fn main() {}
    543"#,
    544            str![[r##"
    545shebang: "#!/usr/bin/env cargo\n"
    546info: None
    547frontmatter: "Hello---\nWorld\n"
    548content: "\nfn main() {}\n"
    549
    550"##]],
    551        );
    552    }
    553
    554    #[test]
    555    fn split_mismatched_dashes() {
    556        assert_err(
    557            ScriptSource::parse(
    558                r#"#!/usr/bin/env cargo
    559---
    560[dependencies]
    561time="0.1.25"
    562----
    563fn main() {}
    564"#,
    565            ),
    566            str!["unexpected trailing content on closing fence: `-`"],
    567        );
    568    }
    569
    570    #[test]
    571    fn split_missing_close() {
    572        assert_err(
    573            ScriptSource::parse(
    574                r#"#!/usr/bin/env cargo
    575---
    576[dependencies]
    577time="0.1.25"
    578fn main() {}
    579"#,
    580            ),
    581            str!["no closing `---` found for frontmatter"],
    582        );
    583    }
    584
    585    #[track_caller]
    586    fn expand(source: &str) -> String {
    587        let shell = crate::Shell::from_write(Box::new(Vec::new()));
    588        let cwd = std::env::current_dir().unwrap();
    589        let home = home::cargo_home_with_cwd(&cwd).unwrap();
    590        let gctx = GlobalContext::new(shell, cwd, home);
    591        expand_manifest(source, std::path::Path::new("/home/me/test.rs"), &gctx)
    592            .unwrap_or_else(|err| panic!("{}", err))
    593    }
    594
    595    #[test]
    596    fn expand_default() {
    597        assert_data_eq!(
    598            expand(r#"fn main() {}"#),
    599            str![[r#"
    600[[bin]]
    601name = "test-"
    602path = "/home/me/test.rs"
    603
    604"#]]
    605        );
    606    }
    607
    608    #[test]
    609    fn expand_dependencies() {
    610        assert_data_eq!(
    611            expand(
    612                r#"---cargo
    613[dependencies]
    614time="0.1.25"
    615---
    616fn main() {}
    617"#
    618            ),
    619            str![[r#"
    620[[bin]]
    621name = "test-"
    622path = [..]
    623
    624[dependencies]
    625time = "0.1.25"
    626
    627"#]]
    628        );
    629    }
    630}