cargo/util/toml/
embedded.rs

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