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}