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_inner_hyphens_1() {
383        assert_source(
384            r#"---
385x ---🚧️
386---
387
388// Regression test for #141483
389//@check-pass
390
391#![feature(frontmatter)]
392
393fn main() {}
394"#,
395            str![[r#"
396shebang: None
397info: None
398frontmatter: "x ---🚧\u{fe0f}\n"
399content: "\n// Regression test for #141483\n//@check-pass\n\n#![feature(frontmatter)]\n\nfn main() {}\n"
400
401"#]],
402        );
403    }
404
405    #[test]
406    fn rustc_frontmatter_inner_hyphens_2() {
407        assert_source(
408            r#"---
409x ---y
410---
411
412// Test that hypens are allowed inside frontmatters if there is some
413// non-whitespace character preceding them.
414//@check-pass
415
416#![feature(frontmatter)]
417
418fn main() {}
419"#,
420            str![[r#"
421shebang: None
422info: None
423frontmatter: "x ---y\n"
424content: "\n// Test that hypens are allowed inside frontmatters if there is some\n// non-whitespace character preceding them.\n//@check-pass\n\n#![feature(frontmatter)]\n\nfn main() {}\n"
425
426"#]],
427        );
428    }
429
430    #[test]
431    fn rustc_frontmatter_non_lexible_tokens() {
432        assert_source(
433            r#"---uwu
434🏳️‍⚧️
435---
436
437//@ check-pass
438
439// check that frontmatter blocks can have tokens that are otherwise not accepted by
440// the lexer as Rust code.
441
442fn main() {}
443"#,
444            str![[r#"
445shebang: None
446info: "uwu"
447frontmatter: "🏳\u{fe0f}\u{200d}⚧\u{fe0f}\n"
448content: "\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"
449
450"#]],
451        );
452    }
453
454    #[test]
455    fn rustc_frontmatter_whitespace_1() {
456        // Deferred to rustc since this requires knowledge of Rust grammar
457        assert_source(
458            r#"  ---
459//~^ ERROR: invalid preceding whitespace for frontmatter opening
460  ---
461//~^ ERROR: invalid preceding whitespace for frontmatter close
462
463// check that whitespaces should not precede the frontmatter opening or close.
464
465fn main() {}
466"#,
467            str![[r#"
468shebang: None
469info: None
470frontmatter: None
471content: "  ---\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"
472
473"#]],
474        );
475    }
476
477    #[test]
478    fn rustc_frontmatter_whitespace_2() {
479        assert_err(
480            ScriptSource::parse(
481                r#"---cargo
482
483//@ compile-flags: --crate-type lib
484
485fn foo(x: i32) -> i32 {
486    ---x
487    //~^ ERROR: invalid preceding whitespace for frontmatter close
488    //~| ERROR: extra characters after frontmatter close are not allowed
489}
490//~^ ERROR: unexpected closing delimiter: `}`
491
492// this test is for the weird case that valid Rust code can have three dashes
493// within them and get treated as a frontmatter close.
494"#,
495            ),
496            str!["no closing `---` found for frontmatter"],
497        );
498    }
499
500    #[test]
501    fn rustc_frontmatter_whitespace_3() {
502        assert_source(
503            r#"
504
505
506---cargo   
507---   
508
509// please note the whitespace characters after the first four lines.
510// This ensures that we accept whitespaces before the frontmatter, after
511// the frontmatter opening and the frontmatter close.
512
513//@ check-pass
514// ignore-tidy-end-whitespace
515// ignore-tidy-leading-newlines
516
517fn main() {}
518"#,
519            str![[r#"
520shebang: None
521info: "cargo"
522frontmatter: ""
523content: "\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"
524
525"#]],
526        );
527    }
528
529    #[test]
530    fn rustc_frontmatter_whitespace_4() {
531        assert_source(
532            r#"--- cargo
533---
534
535//@ check-pass
536// A frontmatter infostring can have leading whitespace.
537
538fn main() {}
539"#,
540            str![[r#"
541shebang: None
542info: "cargo"
543frontmatter: ""
544content: "\n//@ check-pass\n// A frontmatter infostring can have leading whitespace.\n\nfn main() {}\n"
545
546"#]],
547        );
548    }
549
550    #[test]
551    fn rustc_hyphen_in_infostring_leading() {
552        // We don't validate infostrings besides `info == "cargo"`
553        assert_source(
554            r#"--- -toml
555//~^ ERROR: invalid infostring for frontmatter
556---
557
558// infostrings cannot have leading hyphens
559
560#![feature(frontmatter)]
561
562fn main() {}
563"#,
564            str![[r#"
565shebang: None
566info: "-toml"
567frontmatter: "//~^ ERROR: invalid infostring for frontmatter\n"
568content: "\n// infostrings cannot have leading hyphens\n\n#![feature(frontmatter)]\n\nfn main() {}\n"
569
570"#]],
571        );
572    }
573
574    #[test]
575    fn rustc_hyphen_in_infostring_non_leading() {
576        assert_source(
577            r#"--- Cargo-toml
578---
579
580// infostrings can contain hyphens as long as a hyphen isn't the first character.
581//@ check-pass
582
583#![feature(frontmatter)]
584
585fn main() {}
586"#,
587            str![[r#"
588shebang: None
589info: "Cargo-toml"
590frontmatter: ""
591content: "\n// infostrings can contain hyphens as long as a hyphen isn't the first character.\n//@ check-pass\n\n#![feature(frontmatter)]\n\nfn main() {}\n"
592
593"#]],
594        );
595    }
596
597    #[test]
598    fn rustc_included_frontmatter() {
599        // Deferred to rustc since this requires knowledge of Rust grammar
600        assert_source(
601            r#"#![feature(frontmatter)]
602
603//@ check-pass
604
605include!("auxiliary/lib.rs");
606
607// auxiliary/lib.rs contains a frontmatter. Ensure that we can use them in an
608// `include!` macro.
609
610fn main() {
611    foo(1);
612}
613"#,
614            str![[r##"
615shebang: None
616info: None
617frontmatter: None
618content: "#![feature(frontmatter)]\n\n//@ check-pass\n\ninclude!(\"auxiliary/lib.rs\");\n\n// auxiliary/lib.rs contains a frontmatter. Ensure that we can use them in an\n// `include!` macro.\n\nfn main() {\n    foo(1);\n}\n"
619
620"##]],
621        );
622    }
623
624    #[test]
625    fn rustc_infostring_fail() {
626        // We don't validate infostrings besides `info == "cargo"`
627        assert_source(
628            r#"
629---cargo,clippy
630//~^ ERROR: invalid infostring for frontmatter
631---
632
633// infostrings can only be a single identifier.
634
635fn main() {}
636"#,
637            str![[r#"
638shebang: None
639info: "cargo,clippy"
640frontmatter: "//~^ ERROR: invalid infostring for frontmatter\n"
641content: "\n// infostrings can only be a single identifier.\n\nfn main() {}\n"
642
643"#]],
644        );
645    }
646
647    #[test]
648    fn rustc_mismatch_1() {
649        assert_err(
650            ScriptSource::parse(
651                r#"---cargo
652//~^ ERROR: frontmatter close does not match the opening
653----
654
655// there must be the same number of dashes for both the opening and the close
656// of the frontmatter.
657
658fn main() {}
659"#,
660            ),
661            str!["trailing characters found after frontmatter close"],
662        );
663    }
664
665    #[test]
666    fn rustc_mismatch_2() {
667        assert_err(
668            ScriptSource::parse(
669                r#"----cargo
670//~^ ERROR: frontmatter close does not match the opening
671---cargo
672//~^ ERROR: extra characters after frontmatter close are not allowed
673
674fn main() {}
675"#,
676            ),
677            str!["no closing `----` found for frontmatter"],
678        );
679    }
680
681    #[test]
682    fn rustc_multifrontmatter_2() {
683        // This should be valid, bug on rustc's side, see rust-lang/rust#141367
684        assert_source(
685            r#"---
686 ---
687//~^ ERROR: invalid preceding whitespace for frontmatter close
688
689 ---
690//~^ ERROR: expected item, found `-`
691// FIXME(frontmatter): make this diagnostic better
692---
693
694fn main() {}
695"#,
696            str![[r#"
697shebang: None
698info: None
699frontmatter: " ---\n//~^ ERROR: invalid preceding whitespace for frontmatter close\n\n ---\n//~^ ERROR: expected item, found `-`\n// FIXME(frontmatter): make this diagnostic better\n"
700content: "\nfn main() {}\n"
701
702"#]],
703        );
704    }
705
706    #[test]
707    fn rustc_multifrontmatter() {
708        assert_err(
709            ScriptSource::parse(
710                r#"---
711---
712
713---
714//~^ ERROR: expected item, found `-`
715// FIXME(frontmatter): make this diagnostic better
716---
717
718// test that we do not parse another frontmatter block after the first one.
719
720fn main() {}
721"#,
722            ),
723            str!["only one frontmatter is supported"],
724        );
725    }
726
727    #[test]
728    fn rustc_shebang() {
729        assert_source(
730            r#"#!/usr/bin/env -S cargo -Zscript
731---
732[dependencies]
733clap = "4"
734---
735
736//@ check-pass
737
738// Shebangs on a file can precede a frontmatter.
739
740fn main () {}
741"#,
742            str![[r##"
743shebang: "#!/usr/bin/env -S cargo -Zscript\n"
744info: None
745frontmatter: "[dependencies]\nclap = \"4\"\n"
746content: "\n//@ check-pass\n\n// Shebangs on a file can precede a frontmatter.\n\nfn main () {}\n"
747
748"##]],
749        );
750    }
751
752    #[test]
753    fn rustc_unclosed_1() {
754        assert_err(
755            ScriptSource::parse(
756                r#"----cargo
757//~^ ERROR: unclosed frontmatter
758
759// This test checks that the #! characters can help us recover a frontmatter
760// close. There should not be a "missing `main` function" error as the rest
761// are properly parsed.
762
763fn main() {}
764"#,
765            ),
766            str!["no closing `----` found for frontmatter"],
767        );
768    }
769
770    #[test]
771    fn rustc_unclosed_2() {
772        assert_err(
773            ScriptSource::parse(
774                r#"----cargo
775//~^ ERROR: unclosed frontmatter
776//~| ERROR: frontmatters are experimental
777
778//@ compile-flags: --crate-type lib
779
780// Leading whitespace on the feature line prevents recovery. However
781// the dashes quoted will not be used for recovery and the entire file
782// should be treated as within the frontmatter block.
783
784fn foo() -> &str {
785    "----"
786}
787"#,
788            ),
789            str!["no closing `----` found for frontmatter"],
790        );
791    }
792
793    #[test]
794    fn rustc_unclosed_3() {
795        assert_err(
796            ScriptSource::parse(
797                r#"----cargo
798//~^ ERROR: frontmatter close does not match the opening
799
800//@ compile-flags: --crate-type lib
801
802// Unfortunate recovery situation. Not really preventable with improving the
803// recovery strategy, but this type of code is rare enough already.
804
805fn foo(x: i32) -> i32 {
806    ---x
807    //~^ ERROR: invalid preceding whitespace for frontmatter close
808    //~| ERROR: extra characters after frontmatter close are not allowed
809}
810//~^ ERROR: unexpected closing delimiter: `}`
811"#,
812            ),
813            str!["no closing `----` found for frontmatter"],
814        );
815    }
816
817    #[test]
818    fn rustc_unclosed_4() {
819        assert_err(
820            ScriptSource::parse(
821                r#"
822----cargo
823//~^ ERROR: unclosed frontmatter
824
825//! Similarly, a module-level content should allow for recovery as well (as
826//! per unclosed-1.rs)
827
828fn main() {}
829"#,
830            ),
831            str!["no closing `----` found for frontmatter"],
832        );
833    }
834
835    #[test]
836    fn rustc_unclosed_5() {
837        assert_err(
838            ScriptSource::parse(
839                r#"----cargo
840//~^ ERROR: unclosed frontmatter
841//~| ERROR: frontmatters are experimental
842
843// Similarly, a use statement should allow for recovery as well (as
844// per unclosed-1.rs)
845
846use std::env;
847
848fn main() {}
849"#,
850            ),
851            str!["no closing `----` found for frontmatter"],
852        );
853    }
854
855    #[test]
856    fn split_default() {
857        assert_source(
858            r#"fn main() {}
859"#,
860            str![[r#"
861shebang: None
862info: None
863frontmatter: None
864content: "fn main() {}\n"
865
866"#]],
867        );
868    }
869
870    #[test]
871    fn split_dependencies() {
872        assert_source(
873            r#"---
874[dependencies]
875time="0.1.25"
876---
877fn main() {}
878"#,
879            str![[r#"
880shebang: None
881info: None
882frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
883content: "fn main() {}\n"
884
885"#]],
886        );
887    }
888
889    #[test]
890    fn split_infostring() {
891        assert_source(
892            r#"---cargo
893[dependencies]
894time="0.1.25"
895---
896fn main() {}
897"#,
898            str![[r#"
899shebang: None
900info: "cargo"
901frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
902content: "fn main() {}\n"
903
904"#]],
905        );
906    }
907
908    #[test]
909    fn split_infostring_whitespace() {
910        assert_source(
911            r#"--- cargo 
912[dependencies]
913time="0.1.25"
914---
915fn main() {}
916"#,
917            str![[r#"
918shebang: None
919info: "cargo"
920frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
921content: "fn main() {}\n"
922
923"#]],
924        );
925    }
926
927    #[test]
928    fn split_shebang() {
929        assert_source(
930            r#"#!/usr/bin/env cargo
931---
932[dependencies]
933time="0.1.25"
934---
935fn main() {}
936"#,
937            str![[r##"
938shebang: "#!/usr/bin/env cargo\n"
939info: None
940frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
941content: "fn main() {}\n"
942
943"##]],
944        );
945    }
946
947    #[test]
948    fn split_crlf() {
949        assert_source(
950            "#!/usr/bin/env cargo\r\n---\r\n[dependencies]\r\ntime=\"0.1.25\"\r\n---\r\nfn main() {}",
951            str![[r##"
952shebang: "#!/usr/bin/env cargo\r\n"
953info: None
954frontmatter: "[dependencies]\r\ntime=\"0.1.25\"\r\n"
955content: "fn main() {}"
956
957"##]],
958        );
959    }
960
961    #[test]
962    fn split_leading_newlines() {
963        assert_source(
964            r#"#!/usr/bin/env cargo
965    
966
967
968---
969[dependencies]
970time="0.1.25"
971---
972
973
974fn main() {}
975"#,
976            str![[r##"
977shebang: "#!/usr/bin/env cargo\n"
978info: None
979frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
980content: "\n\nfn main() {}\n"
981
982"##]],
983        );
984    }
985
986    #[test]
987    fn split_attribute() {
988        assert_source(
989            r#"#[allow(dead_code)]
990---
991[dependencies]
992time="0.1.25"
993---
994fn main() {}
995"#,
996            str![[r##"
997shebang: None
998info: None
999frontmatter: None
1000content: "#[allow(dead_code)]\n---\n[dependencies]\ntime=\"0.1.25\"\n---\nfn main() {}\n"
1001
1002"##]],
1003        );
1004    }
1005
1006    #[test]
1007    fn split_extra_dash() {
1008        assert_source(
1009            r#"#!/usr/bin/env cargo
1010----------
1011[dependencies]
1012time="0.1.25"
1013----------
1014
1015fn main() {}"#,
1016            str![[r##"
1017shebang: "#!/usr/bin/env cargo\n"
1018info: None
1019frontmatter: "[dependencies]\ntime=\"0.1.25\"\n"
1020content: "\nfn main() {}"
1021
1022"##]],
1023        );
1024    }
1025
1026    #[test]
1027    fn split_too_few_dashes() {
1028        assert_err(
1029            ScriptSource::parse(
1030                r#"#!/usr/bin/env cargo
1031--
1032[dependencies]
1033time="0.1.25"
1034--
1035fn main() {}
1036"#,
1037            ),
1038            str!["found 2 `-` in rust frontmatter, expected at least 3"],
1039        );
1040    }
1041
1042    #[test]
1043    fn split_indent() {
1044        assert_source(
1045            r#"#!/usr/bin/env cargo
1046    ---
1047    [dependencies]
1048    time="0.1.25"
1049    ----
1050
1051fn main() {}
1052"#,
1053            str![[r##"
1054shebang: "#!/usr/bin/env cargo\n"
1055info: None
1056frontmatter: None
1057content: "    ---\n    [dependencies]\n    time=\"0.1.25\"\n    ----\n\nfn main() {}\n"
1058
1059"##]],
1060        );
1061    }
1062
1063    #[test]
1064    fn split_escaped() {
1065        assert_source(
1066            r#"#!/usr/bin/env cargo
1067-----
1068---
1069---
1070-----
1071
1072fn main() {}
1073"#,
1074            str![[r##"
1075shebang: "#!/usr/bin/env cargo\n"
1076info: None
1077frontmatter: "---\n---\n"
1078content: "\nfn main() {}\n"
1079
1080"##]],
1081        );
1082    }
1083
1084    #[test]
1085    fn split_invalid_escaped() {
1086        assert_err(
1087            ScriptSource::parse(
1088                r#"#!/usr/bin/env cargo
1089---
1090-----
1091-----
1092---
1093
1094fn main() {}
1095"#,
1096            ),
1097            str!["trailing characters found after frontmatter close"],
1098        );
1099    }
1100
1101    #[test]
1102    fn split_dashes_in_body() {
1103        assert_source(
1104            r#"#!/usr/bin/env cargo
1105---
1106Hello---
1107World
1108---
1109
1110fn main() {}
1111"#,
1112            str![[r##"
1113shebang: "#!/usr/bin/env cargo\n"
1114info: None
1115frontmatter: "Hello---\nWorld\n"
1116content: "\nfn main() {}\n"
1117
1118"##]],
1119        );
1120    }
1121
1122    #[test]
1123    fn split_mismatched_dashes() {
1124        assert_err(
1125            ScriptSource::parse(
1126                r#"#!/usr/bin/env cargo
1127---
1128[dependencies]
1129time="0.1.25"
1130----
1131fn main() {}
1132"#,
1133            ),
1134            str!["trailing characters found after frontmatter close"],
1135        );
1136    }
1137
1138    #[test]
1139    fn split_missing_close() {
1140        assert_err(
1141            ScriptSource::parse(
1142                r#"#!/usr/bin/env cargo
1143---
1144[dependencies]
1145time="0.1.25"
1146fn main() {}
1147"#,
1148            ),
1149            str!["no closing `---` found for frontmatter"],
1150        );
1151    }
1152
1153    #[track_caller]
1154    fn expand(source: &str) -> String {
1155        expand_manifest(source).unwrap_or_else(|err| panic!("{}", err))
1156    }
1157
1158    #[test]
1159    fn expand_default() {
1160        assert_data_eq!(expand(r#"fn main() {}"#), str![""]);
1161    }
1162
1163    #[test]
1164    fn expand_dependencies() {
1165        assert_data_eq!(
1166            expand(
1167                r#"---cargo
1168[dependencies]
1169time="0.1.25"
1170---
1171fn main() {}
1172"#
1173            ),
1174            str![[r#"
1175[dependencies]
1176time="0.1.25"
1177
1178"#]]
1179        );
1180    }
1181}