1use smol_str::SmolStr;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct AutoloadEntry {
19 pub name: SmolStr,
21 pub path: SmolStr,
23 pub is_singleton: bool,
26}
27
28#[must_use]
31pub fn parse_autoloads(text: &str) -> Vec<AutoloadEntry> {
32 let mut entries = Vec::new();
33 let mut in_autoload = false;
34 for raw_line in text.lines() {
35 let line = raw_line.trim();
36 if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
38 continue;
39 }
40 if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
42 in_autoload = inner.trim() == "autoload";
43 continue;
44 }
45 if !in_autoload {
46 continue;
47 }
48 let Some((name, value)) = line.split_once('=') else {
50 continue;
51 };
52 let name = name.trim();
53 if name.is_empty() {
54 continue;
55 }
56 let value = dequote(value.trim());
58 let (is_singleton, path) = match value.strip_prefix('*') {
59 Some(rest) => (true, rest),
60 None => (false, value),
61 };
62 if path.is_empty() {
63 continue;
64 }
65 entries.push(AutoloadEntry {
66 name: SmolStr::new(name),
67 path: SmolStr::new(path),
68 is_singleton,
69 });
70 }
71 entries
72}
73
74fn dequote(s: &str) -> &str {
76 let bytes = s.as_bytes();
77 if bytes.len() >= 2
78 && (bytes[0] == b'"' || bytes[0] == b'\'')
79 && bytes[bytes.len() - 1] == bytes[0]
80 {
81 &s[1..s.len() - 1]
82 } else {
83 s
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn parses_singleton_and_strips_star() {
93 let e = parse_autoloads("[autoload]\nGame=\"*res://game.gd\"\n");
94 assert_eq!(e.len(), 1);
95 assert_eq!(e[0].name, "Game");
96 assert_eq!(e[0].path, "res://game.gd");
97 assert!(e[0].is_singleton);
98 }
99
100 #[test]
101 fn non_star_is_not_a_singleton() {
102 let e = parse_autoloads("[autoload]\nHelper=\"res://helper.gd\"\n");
103 assert_eq!(e.len(), 1);
104 assert_eq!(e[0].path, "res://helper.gd");
105 assert!(!e[0].is_singleton, "no leading * → loaded-but-not-global");
106 }
107
108 #[test]
109 fn only_the_autoload_section_is_read() {
110 let src = "config_version=5\n\
111 [application]\n\
112 config/name=\"Demo\"\n\
113 config/features=PackedStringArray(\"4.6\")\n\
114 \n\
115 [autoload]\n\
116 ; a comment\n\
117 Log=\"*res://utils/system_log.gd\"\n\
118 Music=\"*res://music.tscn\"\n\
119 \n\
120 [rendering]\n\
121 renderer/rendering_method=\"gl_compatibility\"\n";
122 let e = parse_autoloads(src);
123 assert_eq!(e.len(), 2);
124 assert_eq!(e[0].name, "Log");
125 assert_eq!(e[0].path, "res://utils/system_log.gd");
126 assert!(e[0].is_singleton);
127 assert_eq!(e[1].name, "Music");
129 assert_eq!(e[1].path, "res://music.tscn");
130 }
132
133 #[test]
134 fn empty_or_no_autoload_section_is_empty() {
135 assert!(parse_autoloads("").is_empty());
136 assert!(parse_autoloads("[application]\nconfig/name=\"X\"\n").is_empty());
137 }
138}