#
cargo/util/toml/
embedded.rs

    1use cargo_util_schemas::manifest::PackageName;
    2
    3use crate::util::restricted_names;
    4use crate::CargoResult;
    5
    6pub(super) fn expand_manifest(content: &str) -> CargoResult<String> {
    7    let source = ScriptSource::parse(content)?;
    8    if let Some(frontmatter) = source.frontmatter() {
    9        match source.info() {
    10            Some("cargo") | None => {}
    11            Some(other) => {
    12                if let Some(remainder) = other.strip_prefix("cargo,") {
    13                    anyhow::bail!("cargo does not support frontmatter infostring attributes like `{remainder}` at this time")
    14                } else {
    15                    anyhow::bail!("frontmatter infostring `{other}` is unsupported by cargo; specify `cargo` for embedding a manifest")
    16                }
    17            }
    18        }
    19
    20        Ok(frontmatter.to_owned())
    21    } else {
    22        let frontmatter = "";
    23        Ok(frontmatter.to_owned())
    24    }
    25}
    26
    27/// Ensure the package name matches the validation from `ops::cargo_new::check_name`
    28pub fn sanitize_name(name: &str) -> String {
    29    let placeholder = if name.contains('_') {
    30        '_'
    31    } else {
    32        // Since embedded manifests only support `[[bin]]`s, prefer arrow-case as that is the
    33        // more common convention for CLIs
    34        '-'
    35    };
    36
    37    let mut name = PackageName::sanitize(name, placeholder).into_inner();
    38
    39    loop {
    40        if restricted_names::is_keyword(&name) {
    41            name.push(placeholder);
    42        } else if restricted_names::is_conflicting_artifact_name(&name) {
    43            // Being an embedded manifest, we always assume it is a `[[bin]]`
    44            name.push(placeholder);
    45        } else if name == "test" {
    46            name.push(placeholder);
    47        } else if restricted_names::is_windows_reserved(&name) {
    48            // Go ahead and be consistent across platforms
    49            name.push(placeholder);
    50        } else {
    51            break;
    52        }
    53    }
    54
    55    name
    56}
    57
    58#[derive(Debug)]
    59pub struct ScriptSource<'s> {
    60    shebang: Option<&'s str>,
    61    info: Option<&'s str>,
    62    frontmatter: Option<&'s str>,
    63    content: &'s str,
    64}
    65
    66impl<'s> ScriptSource<'s> {
    67    pub fn parse(input: &'s str) -> CargoResult<Self> {
    68        let mut source = Self {
    69            shebang: None,
    70            info: None,
    71            frontmatter: None,
    72            content: input,
    73        };
    74
    75        if let Some(shebang_end) = strip_shebang(source.content) {
    76            let (shebang, content) = source.content.split_at(shebang_end);
    77            source.shebang = Some(shebang);
    78            source.content = content;
    79        }
    80
    81        let mut rest = source.content;
    82
    83        // Whitespace may precede a frontmatter but must end with a newline
    84        let trimmed = rest.trim_start_matches(is_whitespace);
    85        if trimmed.len() != rest.len() {
    86            let trimmed_len = rest.len() - trimmed.len();
    87            let last_trimmed_index = trimmed_len - 1;
    88            if rest.as_bytes()[last_trimmed_index] != b'\n' {
    89                // either not a frontmatter or invalid opening
    90                return Ok(source);
    91            }
    92        }
    93        rest = trimmed;
    94
    95        // Opens with a line that starts with 3 or more `-` followed by an optional identifier
    96        const FENCE_CHAR: char = '-';
    97        let fence_length = rest
    98            .char_indices()
    99            .find_map(|(i, c)| (c != FENCE_CHAR).then_some(i))
    100            .unwrap_or(rest.len());
    101        match fence_length {
    102            0 => {
    103                return Ok(source);
    104            }
    105            1 | 2 => {
    106                // either not a frontmatter or invalid frontmatter opening
    107                anyhow::bail!(
    108                    "found {fence_length} `{FENCE_CHAR}` in rust frontmatter, expected at least 3"
    109                )
    110            }
    111            _ => {}
    112        }
    113        let (fence_pattern, rest) = rest.split_at(fence_length);
    114        let Some(info_end_index) = rest.find('\n') else {
    115            anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
    116        };
    117        let (info, rest) = rest.split_at(info_end_index);
    118        let info = info.trim_matches(is_whitespace);
    119        if !info.is_empty() {
    120            source.info = Some(info);
    121        }
    122
    123        // Ends with a line that starts with a matching number of `-` only followed by whitespace
    124        let nl_fence_pattern = format!("\n{fence_pattern}");
    125        let Some(frontmatter_nl) = rest.find(&nl_fence_pattern) else {
    126            anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
    127        };
    128        let frontmatter = &rest[..frontmatter_nl + 1];
    129        let frontmatter = frontmatter
    130            .strip_prefix('\n')
    131            .expect("earlier `found` + `split_at` left us here");
    132        source.frontmatter = Some(frontmatter);
    133        let rest = &rest[frontmatter_nl + nl_fence_pattern.len()..];
    134
    135        let (after_closing_fence, rest) = rest.split_once("\n").unwrap_or((rest, ""));
    136        let after_closing_fence = after_closing_fence.trim_matches(is_whitespace);
    137        if !after_closing_fence.is_empty() {
    138            // extra characters beyond the original fence pattern, even if they are extra `-`
    139            anyhow::bail!("trailing characters found after frontmatter close");
    140        }
    141
    142        let frontmatter_len = input.len() - rest.len();
    143        source.content = &input[frontmatter_len..];
    144
    145        let repeat = Self::parse(source.content)?;
    146        if repeat.frontmatter.is_some() {
    147            anyhow::bail!("only one frontmatter is supported");
    148        }
    149
    150        Ok(source)
    151    }
    152
    153    pub fn shebang(&self) -> Option<&'s str> {
    154        self.shebang
    155    }
    156
    157    pub fn info(&self) -> Option<&'s str> {
    158        self.info
    159    }
    160
    161    pub fn frontmatter(&self) -> Option<&'s str> {
    162        self.frontmatter
    163    }
    164
    165    pub fn content(&self) -> &'s str {
    166        self.content
    167    }
    168}
    169
    170fn strip_shebang(input: &str) -> Option<usize> {
    171    // See rust-lang/rust's compiler/rustc_lexer/src/lib.rs's `strip_shebang`
    172    // Shebang must start with `#!` literally, without any preceding whitespace.
    173    // For simplicity we consider any line starting with `#!` a shebang,
    174    // regardless of restrictions put on shebangs by specific platforms.
    175    if let Some(rest) = input.strip_prefix("#!") {
    176        // Ok, this is a shebang but if the next non-whitespace token is `[`,
    177        // then it may be valid Rust code, so consider it Rust code.
    178        //
    179        // NOTE: rustc considers line and block comments to be whitespace but to avoid
    180        // any more awareness of Rust grammar, we are excluding it.
    181        if !rest.trim_start().starts_with('[') {
    182            // No other choice than to consider this a shebang.
    183            let newline_end = input.find('\n').map(|pos| pos + 1).unwrap_or(input.len());
    184            return Some(newline_end);
    185        }
    186    }
    187    None
    188}
    189
    190/// True if `c` is considered a whitespace according to Rust language definition.
    191/// See [Rust language reference](https://doc.rust-lang.org/reference/whitespace.html)
    192/// for definitions of these classes.
    193///
    194/// See rust-lang/rust's compiler/rustc_lexer/src/lib.rs `is_whitespace`
    195fn is_whitespace(c: char) -> bool {
    196    // This is Pattern_White_Space.
    197    //
    198    // Note that this set is stable (ie, it doesn't change with different
    199    // Unicode versions), so it's ok to just hard-code the values.
    200
    201    matches!(
    202        c,
    203        // Usual ASCII suspects
    204        '\u{0009}'   // \t
    205        | '\u{000A}' // \n
    206        | '\u{000B}' // vertical tab
    207        | '\u{000C}' // form feed
    208        | '\u{000D}' // \r
    209        | '\u{0020}' // space
    210
    211        // NEXT LINE from latin1
    212        | '\u{0085}'
    213
    214        // Bidi markers
    215        | '\u{200E}' // LEFT-TO-RIGHT MARK
    216        | '\u{200F}' // RIGHT-TO-LEFT MARK
    217
    218        // Dedicated whitespace characters from Unicode
    219        | '\u{2028}' // LINE SEPARATOR
    220        | '\u{2029}' // PARAGRAPH SEPARATOR
    221    )
    222}
    223
    224#[cfg(test)]
    225mod test_expand {
    226    use snapbox::assert_data_eq;
    227    use snapbox::prelude::*;
    228    use snapbox::str;
    229
    230    use super::*;
    231
    232    #[track_caller]
    233    fn assert_source(source: &str, expected: impl IntoData) {
    234        use std::fmt::Write as _;
    235
    236        let actual = match ScriptSource::parse(source) {
    237            Ok(actual) => actual,
    238            Err(err) => panic!("unexpected err: {err}"),
    239        };
    240
    241        let mut rendered = String::new();
    242        write_optional_field(&mut rendered, "shebang", actual.shebang());
    243        write_optional_field(&mut rendered, "info", actual.info());
    244        write_optional_field(&mut rendered, "frontmatter", actual.frontmatter());
    245        writeln!(&mut rendered, "content: {:?}", actual.content()).unwrap();
    246        assert_data_eq!(rendered, expected.raw());
    247    }
    248
    249    fn write_optional_field(writer: &mut dyn std::fmt::Write, field: &str, value: Option<&str>) {
    250        if let Some(value) = value {
    251            writeln!(writer, "{field}: {value:?}").unwrap();
    252        } else {
    253            writeln!(writer, "{field}: None").unwrap();
    254        }
    255    }
    256
    257    #[track_caller]
    258    fn assert_err(
    259        result: Result<impl std::fmt::Debug, impl std::fmt::Display>,
    260        err: impl IntoData,
    261    ) {
    262        match result {
    263            Ok(d) => panic!("unexpected Ok({d:#?})"),
    264            Err(actual) => snapbox::assert_data_eq!(actual.to_string(), err.raw()),
    265        }
    266    }
    267
    268    #[test]
    269    fn rustc_dot_in_infostring_leading() {
    270        // We don't validate infostrings besides `info == "cargo"`
    271        assert_source(
    272            r#"---.toml
    273//~^ ERROR: invalid infostring for frontmatter
    274---
    275
    276// infostrings cannot have leading dots
    277
    278fn main() {}
    279"#,
    280            str![[r#"
    281shebang: None
    282info: ".toml"
    283frontmatter: "//~^ ERROR: invalid infostring for frontmatter\n"
    284content: "\n// infostrings cannot have leading dots\n\nfn main() {}\n"
    285
    286"#]],
    287        );
    288    }
    289
    290    #[test]
    291    fn rustc_dot_in_infostring_non_leading() {
    292        assert_source(
    293            r#"---Cargo.toml
    294---
    295
    296// infostrings can contain dots as long as a dot isn't the first character.
    297//@ check-pass
    298
    299fn main() {}
    300"#,
    301            str![[r#"
    302shebang: None
    303info: "Cargo.toml"
    304frontmatter: ""
    305content: "\n// infostrings can contain dots as long as a dot isn't the first character.\n//@ check-pass\n\nfn main() {}\n"
    306
    307"#]],
    308        );
    309    }
    310
    311    #[test]
    312    fn rustc_escape() {
    313        assert_source(
    314            r#"----
    315
    316---
    317
    318----
    319
    320//@ check-pass
    321
    322// This test checks that longer dashes for opening and closing can be used to
    323// escape sequences such as three dashes inside the frontmatter block.
    324
    325fn main() {}
    326"#,
    327            str![[r#"
    328shebang: None
    329info: None
    330frontmatter: "\n---\n\n"
    331content: "\n//@ check-pass\n\n// This test checks that longer dashes for opening and closing can be used to\n// escape sequences such as three dashes inside the frontmatter block.\n\nfn main() {}\n"
    332
    333"#]],
    334        );
    335    }
    336
    337    #[test]
    338    fn rustc_extra_after_end() {
    339        assert_err(
    340            ScriptSource::parse(
    341                r#"---
    342---cargo
    343//~^ ERROR: extra characters after frontmatter close are not allowed
    344
    345fn main() {}
    346"#,
    347            ),
    348            str!["trailing characters found after frontmatter close"],
    349        );
    350    }
    351
    352    #[test]
    353    fn rustc_frontmatter_after_tokens() {
    354        // Deferred to rustc since this requires knowledge of Rust grammar
    355        assert_source(
    356            r#"#![feature(frontmatter)]
    357
    358---
    359//~^ ERROR: expected item, found `-`
    360// FIXME(frontmatter): make this diagnostic better
    361---
    362
    363// frontmatters must be at the start of a file. This test ensures that.
    364
    365fn main() {}
    366"#,
    367            str![[r##"
    368shebang: None
    369info: None
    370frontmatter: None
    371content: "#![feature(frontmatter)]\n\n---\n//~^ ERROR: expected item, found `-`\n// FIXME(frontmatter): make this diagnostic better\n---\n\n// frontmatters must be at the start of a file. This test ensures that.\n\nfn main() {}\n"
    372
    373"##]],
    374        );
    375    }
    376
    377    #[test]
    378    fn rustc_frontmatter_non_lexible_tokens() {
    379        assert_source(
    380            r#"---uwu
    381🏳️‍⚧️
    382---
    383
    384//@ check-pass
    385
    386// check that frontmatter blocks can have tokens that are otherwise not accepted by
    387// the lexer as Rust code.
    388
    389fn main() {}
    390"#,
    391            str![[r#"
    392shebang: None
    393info: "uwu"
    394frontmatter: "🏳\u{fe0f}\u{200d}⚧\u{fe0f}\n"
    395content: "\n//@ check-pass\n\n// check that frontmatter blocks can have tokens that are otherwise not accepted by\n// the lexer as Rust code.\n\nfn main() {}\n"
    396
    397"#]],
    398        );
    399    }
    400
    401    #[test]
    402    fn rustc_frontmatter_whitespace_1() {
    403        // Deferred to rustc since this requires knowledge of Rust grammar
    404        assert_source(
    405            r#"  ---
    406//~^ ERROR: invalid preceding whitespace for frontmatter opening
    407  ---
    408//~^ ERROR: invalid preceding whitespace for frontmatter close
    409
    410// check that whitespaces should not precede the frontmatter opening or close.
    411
    412fn main() {}
    413"#,
    414            str![[r#"
    415shebang: None
    416info: None
    417frontmatter: None
    418content: "  ---\n//~^ ERROR: invalid preceding whitespace for frontmatter opening\n  ---\n//~^ ERROR: invalid preceding whitespace for frontmatter close\n\n// check that whitespaces should not precede the frontmatter opening or close.\n\nfn main() {}\n"
    419
    420"#]],
    421        );
    422    }
    423
    424    #[test]
    425    fn rustc_frontmatter_whitespace_2() {
    426        assert_err(
    427            ScriptSource::parse(
    428                r#"---cargo
    429
    430//@ compile-flags: --crate-type lib
    431
    432fn foo(x: i32) -> i32 {
    433    ---x
    434    //~^ ERROR: invalid preceding whitespace for frontmatter close
    435    //~| ERROR: extra characters after frontmatter close are not allowed
    436}
    437//~^ ERROR: unexpected closing delimiter: `}`
    438
    439// this test is for the weird case that valid Rust code can have three dashes
    440// within them and get treated as a frontmatter close.
    441"#,
    442            ),
    443            str!["no closing `---` found for frontmatter"],
    444        );
    445    }
    446
    447    #[test]
    448    fn rustc_frontmatter_whitespace_3() {
    449        assert_source(
    450            r#"
    451
    452
    453---cargo
    454---
    455
    456// please note the whitespace characters after the first four lines.
    457// This ensures that we accept whitespaces before the frontmatter, after
    458// the frontmatter opening and the frontmatter close.
    459
    460//@ check-pass
    461// ignore-tidy-end-whitespace
    462// ignore-tidy-leading-newlines
    463
    464fn main() {}
    465"#,
    466            str![[r#"
    467shebang: None
    468info: "cargo"
    469frontmatter: ""
    470content: "\n// please note the whitespace characters after the first four lines.\n// This ensures that we accept whitespaces before the frontmatter, after\n// the frontmatter opening and the frontmatter close.\n\n//@ check-pass\n// ignore-tidy-end-whitespace\n// ignore-tidy-leading-newlines\n\nfn main() {}\n"
    471
    472"#]],
    473        );
    474    }
    475
    476    #[test]
    477    fn rustc_frontmatter_whitespace_4() {
    478        assert_source(
    479            r#"--- cargo
    480---
    481
    482//@ check-pass
    483// A frontmatter infostring can have leading whitespace.
    484
    485fn main() {}
    486"#,
    487            str![[r#"
    488shebang: None
    489info: "cargo"
    490frontmatter: ""
    491content: "\n//@ check-pass\n// A frontmatter infostring can have leading whitespace.\n\nfn main() {}\n"
    492
    493"#]],
    494        );
    495    }
    496
    497    #[test]
    498    fn rustc_infostring_fail() {
    499        // We don't validate infostrings besides `info == "cargo"`
    500        assert_source(
    501            r#"
    502---cargo,clippy
    503//~^ ERROR: invalid infostring for frontmatter
    504---
    505
    506// infostrings can only be a single identifier.
    507
    508fn main() {}
    509"#,
    510            str![[r#"
    511shebang: None
    512info: "cargo,clippy"
    513frontmatter: "//~^ ERROR: invalid infostring for frontmatter\n"
    514content: "\n// infostrings can only be a single identifier.\n\nfn main() {}\n"
    515
    516"#]],
    517        );
    518    }
    519
    520    #[test]
    521    fn rustc_mismatch_1() {
    522        assert_err(
    523            ScriptSource::parse(
    524                r#"---cargo
    525//~^ ERROR: frontmatter close does not match the opening
    526----
    527
    528// there must be the same number of dashes for both the opening and the close
    529// of the frontmatter.
    530
    531fn main() {}
    532"#,
    533            ),
    534            str!["trailing characters found after frontmatter close"],
    535        );
    536    }
    537
    538    #[test]
    539    fn rustc_mismatch_2() {
    540        assert_err(
    541            ScriptSource::parse(
    542                r#"----cargo
    543//~^ ERROR: frontmatter close does not match the opening
    544---cargo
    545//~^ ERROR: extra characters after frontmatter close are not allowed
    546
    547fn main() {}
    548"#,
    549            ),
    550            str!["no closing `----` found for frontmatter"],
    551        );
    552    }
    553
    554    #[test]
    555    fn rustc_multifrontmatter_2() {
    556        // This should be valid, bug on rustc's side, see rust-lang/rust#141367
    557        assert_source(
    558            r#"---
    559 ---
    560//~^ ERROR: invalid preceding whitespace for frontmatter close
    561
    562 ---
    563//~^ ERROR: expected item, found `-`
    564// FIXME(frontmatter): make this diagnostic better
    565---
    566
    567fn main() {}
    568"#,
    569            str![[r#"
    570shebang: None
    571info: None
    572frontmatter: " ---\n//~^ ERROR: invalid preceding whitespace for frontmatter close\n\n ---\n//~^ ERROR: expected item, found `-`\n// FIXME(frontmatter): make this diagnostic better\n"
    573content: "\nfn main() {}\n"
    574
    575"#]],
    576        );
    577    }
    578
    579    #[test]
    580    fn rustc_multifrontmatter() {
    581        assert_err(
    582            ScriptSource::parse(
    583                r#"---
    584---
    585
    586---
    587//~^ ERROR: expected item, found `-`
    588// FIXME(frontmatter): make this diagnostic better
    589---
    590
    591// test that we do not parse another frontmatter block after the first one.
    592
    593fn main() {}
    594"#,
    595            ),
    596            str!["only one frontmatter is supported"],
    597        );
    598    }
    599
    600    #[test]
    601    fn rustc_shebang() {
    602        assert_source(
    603            r#"#!/usr/bin/env -S cargo -Zscript
    604---
    605[dependencies]
    606clap = "4"
    607---
    608
    609//@ check-pass
    610
    611// Shebangs on a file can precede a frontmatter.
    612
    613fn main () {}
    614"#,
    615            str![[r##"
    616shebang: "#!/usr/bin/env -S cargo -Zscript\n"
    617info: None
    618frontmatter: "[dependencies]\nclap = \"4\"\n"
    619content: "\n//@ check-pass\n\n// Shebangs on a file can precede a frontmatter.\n\nfn main () {}\n"
    620
    621"##]],
    622        );
    623    }
    624
    625    #[test]
    626    fn rustc_unclosed_1() {
    627        assert_err(
    628            ScriptSource::parse(
    629                r#"----cargo
    630//~^ ERROR: unclosed frontmatter
    631
    632// This test checks that the #! characters can help us recover a frontmatter
    633// close. There should not be a "missing `main` function" error as the rest
    634// are properly parsed.
    635
    636fn main() {}
    637"#,
    638            ),
    639            str!["no closing `----` found for frontmatter"],
    640        );
    641    }
    642
    643    #[test]
    644    fn rustc_unclosed_2() {
    645        assert_err(
    646            ScriptSource::parse(
    647                r#"----cargo
    648//~^ ERROR: unclosed frontmatter
    649//~| ERROR: frontmatters are experimental
    650
    651//@ compile-flags: --crate-type lib
    652
    653// Leading whitespace on the feature line prevents recovery. However
    654// the dashes quoted will not be used for recovery and the entire file
    655// should be treated as within the frontmatter block.
    656
    657fn foo() -> &str {
    658    "----"
    659}
    660"#,
    661            ),
    662            str!["no closing `----` found for frontmatter"],
    663        );
    664    }
    665
    666    #[test]
    667    fn rustc_unclosed_3() {
    668        assert_err(
    669            ScriptSource::parse(
    670                r#"----cargo
    671//~^ ERROR: frontmatter close does not match the opening
    672
    673//@ compile-flags: --crate-type lib
    674
    675// Unfortunate recovery situation. Not really preventable with improving the
    676// recovery strategy, but this type of code is rare enough already.
    677
    678fn foo(x: i32) -> i32 {
    679    ---x
    680    //~^ ERROR: invalid preceding whitespace for frontmatter close
    681    //~| ERROR: extra characters after frontmatter close are not allowed
    682}
    683//~^ ERROR: unexpected closing delimiter: `}`
    684"#,
    685            ),
    686            str!["no closing `----` found for frontmatter"],
    687        );
    688    }
    689
    690    #[test]
    691    fn rustc_unclosed_4() {
    692        assert_err(
    693            ScriptSource::parse(
    694                r#"
    695----cargo
    696//~^ ERROR: unclosed frontmatter
    697
    698//! Similarly, a module-level content should allow for recovery as well (as
    699//! per unclosed-1.rs)
    700
    701fn main() {}
    702"#,
    703            ),
    704            str!["no closing `----` found for frontmatter"],
    705        );
    706    }
    707
    708    #[test]
    709    fn rustc_unclosed_5() {
    710        assert_err(
    711            ScriptSource::parse(
    712                r#"----cargo
    713//~^ ERROR: unclosed frontmatter
    714//~| ERROR: frontmatters are experimental
    715
    716// Similarly, a use statement should allow for recovery as well (as
    717// per unclosed-1.rs)
    718
    719use std::env;
    720
    721fn main() {}
    722"#,
    723            ),
    724            str!["no closing `----` found for frontmatter"],
    725        );
    726    }
    727
    728    #[test]
    729    fn split_default() {
    730        assert_source(
    731            r#"fn main() {}
    732"#,
    733            str![[r#"
    734shebang: None
    735info: None
    736frontmatter: None
    737content: "fn main() {}\n"
    738
    739"#]],
    740        );
    741    }
    742
    743    #[test]
    744    fn split_dependencies() {
    745        assert_source(
    746            r#"---
    747[dependencies]
    748time="0.1.25"
    749---
    750fn main() {}
    751"#,
    752            str![[r#"
    753shebang: None
    754info: None
    755frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    756content: "fn main() {}\n"
    757
    758"#]],
    759        );
    760    }
    761
    762    #[test]
    763    fn split_infostring() {
    764        assert_source(
    765            r#"---cargo
    766[dependencies]
    767time="0.1.25"
    768---
    769fn main() {}
    770"#,
    771            str![[r#"
    772shebang: None
    773info: "cargo"
    774frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    775content: "fn main() {}\n"
    776
    777"#]],
    778        );
    779    }
    780
    781    #[test]
    782    fn split_infostring_whitespace() {
    783        assert_source(
    784            r#"--- cargo
    785[dependencies]
    786time="0.1.25"
    787---
    788fn main() {}
    789"#,
    790            str![[r#"
    791shebang: None
    792info: "cargo"
    793frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    794content: "fn main() {}\n"
    795
    796"#]],
    797        );
    798    }
    799
    800    #[test]
    801    fn split_shebang() {
    802        assert_source(
    803            r#"#!/usr/bin/env cargo
    804---
    805[dependencies]
    806time="0.1.25"
    807---
    808fn main() {}
    809"#,
    810            str![[r##"
    811shebang: "#!/usr/bin/env cargo\n"
    812info: None
    813frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    814content: "fn main() {}\n"
    815
    816"##]],
    817        );
    818    }
    819
    820    #[test]
    821    fn split_crlf() {
    822        assert_source(
    823                "#!/usr/bin/env cargo\r\n---\r\n[dependencies]\r\ntime=\"0.1.25\"\r\n---\r\nfn main() {}",
    824            str![[r##"
    825shebang: "#!/usr/bin/env cargo\r\n"
    826info: None
    827frontmatter: "[dependencies]\r\ntime=\"0.1.25\"\r\n"
    828content: "fn main() {}"
    829
    830"##]]
    831        );
    832    }
    833
    834    #[test]
    835    fn split_leading_newlines() {
    836        assert_source(
    837            r#"#!/usr/bin/env cargo
    838
    839
    840
    841---
    842[dependencies]
    843time="0.1.25"
    844---
    845
    846
    847fn main() {}
    848"#,
    849            str![[r##"
    850shebang: "#!/usr/bin/env cargo\n"
    851info: None
    852frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    853content: "\n\nfn main() {}\n"
    854
    855"##]],
    856        );
    857    }
    858
    859    #[test]
    860    fn split_attribute() {
    861        assert_source(
    862            r#"#[allow(dead_code)]
    863---
    864[dependencies]
    865time="0.1.25"
    866---
    867fn main() {}
    868"#,
    869            str![[r##"
    870shebang: None
    871info: None
    872frontmatter: None
    873content: "#[allow(dead_code)]\n---\n[dependencies]\ntime=\"0.1.25\"\n---\nfn main() {}\n"
    874
    875"##]],
    876        );
    877    }
    878
    879    #[test]
    880    fn split_extra_dash() {
    881        assert_source(
    882            r#"#!/usr/bin/env cargo
    883----------
    884[dependencies]
    885time="0.1.25"
    886----------
    887
    888fn main() {}"#,
    889            str![[r##"
    890shebang: "#!/usr/bin/env cargo\n"
    891info: None
    892frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
    893content: "\nfn main() {}"
    894
    895"##]],
    896        );
    897    }
    898
    899    #[test]
    900    fn split_too_few_dashes() {
    901        assert_err(
    902            ScriptSource::parse(
    903                r#"#!/usr/bin/env cargo
    904--
    905[dependencies]
    906time="0.1.25"
    907--
    908fn main() {}
    909"#,
    910            ),
    911            str!["found 2 `-` in rust frontmatter, expected at least 3"],
    912        );
    913    }
    914
    915    #[test]
    916    fn split_indent() {
    917        assert_source(
    918            r#"#!/usr/bin/env cargo
    919    ---
    920    [dependencies]
    921    time="0.1.25"
    922    ----
    923
    924fn main() {}
    925"#,
    926            str![[r##"
    927shebang: "#!/usr/bin/env cargo\n"
    928info: None
    929frontmatter: None
    930content: "    ---\n    [dependencies]\n    time=\"0.1.25\"\n    ----\n\nfn main() {}\n"
    931
    932"##]],
    933        );
    934    }
    935
    936    #[test]
    937    fn split_escaped() {
    938        assert_source(
    939            r#"#!/usr/bin/env cargo
    940-----
    941---
    942---
    943-----
    944
    945fn main() {}
    946"#,
    947            str![[r##"
    948shebang: "#!/usr/bin/env cargo\n"
    949info: None
    950frontmatter: "---\n---\n"
    951content: "\nfn main() {}\n"
    952
    953"##]],
    954        );
    955    }
    956
    957    #[test]
    958    fn split_invalid_escaped() {
    959        assert_err(
    960            ScriptSource::parse(
    961                r#"#!/usr/bin/env cargo
    962---
    963-----
    964-----
    965---
    966
    967fn main() {}
    968"#,
    969            ),
    970            str!["trailing characters found after frontmatter close"],
    971        );
    972    }
    973
    974    #[test]
    975    fn split_dashes_in_body() {
    976        assert_source(
    977            r#"#!/usr/bin/env cargo
    978---
    979Hello---
    980World
    981---
    982
    983fn main() {}
    984"#,
    985            str![[r##"
    986shebang: "#!/usr/bin/env cargo\n"
    987info: None
    988frontmatter: "Hello---\nWorld\n"
    989content: "\nfn main() {}\n"
    990
    991"##]],
    992        );
    993    }
    994
    995    #[test]
    996    fn split_mismatched_dashes() {
    997        assert_err(
    998            ScriptSource::parse(
    999                r#"#!/usr/bin/env cargo
    1000---
    1001[dependencies]
    1002time="0.1.25"
    1003----
    1004fn main() {}
    1005"#,
    1006            ),
    1007            str!["trailing characters found after frontmatter close"],
    1008        );
    1009    }
    1010
    1011    #[test]
    1012    fn split_missing_close() {
    1013        assert_err(
    1014            ScriptSource::parse(
    1015                r#"#!/usr/bin/env cargo
    1016---
    1017[dependencies]
    1018time="0.1.25"
    1019fn main() {}
    1020"#,
    1021            ),
    1022            str!["no closing `---` found for frontmatter"],
    1023        );
    1024    }
    1025
    1026    #[track_caller]
    1027    fn expand(source: &str) -> String {
    1028        expand_manifest(source).unwrap_or_else(|err| panic!("{}", err))
    1029    }
    1030
    1031    #[test]
    1032    fn expand_default() {
    1033        assert_data_eq!(expand(r#"fn main() {}"#), str![""]);
    1034    }
    1035
    1036    #[test]
    1037    fn expand_dependencies() {
    1038        assert_data_eq!(
    1039            expand(
    1040                r#"---cargo
    1041[dependencies]
    1042time="0.1.25"
    1043---
    1044fn main() {}
    1045"#
    1046            ),
    1047            str![[r#"
    1048[dependencies]
    1049time="0.1.25"
    1050
    1051"#]]
    1052        );
    1053    }
    1054}