1use gdscript_base::TextRange;
14use rustc_hash::{FxHashMap, FxHashSet};
15use smol_str::SmolStr;
16
17use crate::model::{
18 ExtId, ExtResource, NodeIdx, NodeProp, SceneConnection, SceneKind, SceneModel, SceneNode,
19 SceneProblem, SubResource,
20};
21
22#[must_use]
24pub fn parse_scene(text: &str) -> SceneModel {
25 if binary_magic(text) {
26 let mut m = SceneModel::empty(SceneKind::Scene);
27 m.problems.push(SceneProblem::BinaryResource);
28 return m;
29 }
30 let mut p = Parser::new(text);
31 p.run();
32 p.build_tree();
33 p.model
34}
35
36fn binary_magic(text: &str) -> bool {
38 let b = text.as_bytes();
39 let mut i = 0;
40 while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\r' | b'\n') {
41 i += 1;
42 }
43 let rest = &b[i..];
44 rest.starts_with(b"RSRC") || rest.starts_with(b"RSCC")
45}
46
47type Span = (usize, usize);
49
50#[derive(Default)]
52struct HeaderAttrs {
53 name: Option<Span>,
54 typ: Option<Span>,
55 parent: Option<Span>,
56 instance: Option<Span>,
57 instance_placeholder: Option<Span>,
58 format: Option<Span>,
59 uid: Option<Span>,
60 script_class: Option<Span>,
61 id: Option<Span>,
62 path: Option<Span>,
63 signal: Option<Span>,
64 from: Option<Span>,
65 to: Option<Span>,
66 method: Option<Span>,
67}
68
69impl HeaderAttrs {
70 fn set(&mut self, key: &str, value: Span) {
71 let slot = match key {
72 "name" => &mut self.name,
73 "type" => &mut self.typ,
74 "parent" => &mut self.parent,
75 "instance" => &mut self.instance,
76 "instance_placeholder" => &mut self.instance_placeholder,
77 "format" => &mut self.format,
78 "uid" => &mut self.uid,
79 "script_class" => &mut self.script_class,
80 "id" => &mut self.id,
81 "path" => &mut self.path,
82 "signal" => &mut self.signal,
83 "from" => &mut self.from,
84 "to" => &mut self.to,
85 "method" => &mut self.method,
86 _ => return, };
88 *slot = Some(value);
89 }
90}
91
92struct Parser<'a> {
93 src: &'a str,
94 bytes: &'a [u8],
95 pos: usize,
96 model: SceneModel,
97}
98
99impl<'a> Parser<'a> {
100 fn new(src: &'a str) -> Self {
101 Self {
102 src,
103 bytes: src.as_bytes(),
104 pos: 0,
105 model: SceneModel::empty(SceneKind::Scene),
106 }
107 }
108
109 fn peek(&self) -> Option<u8> {
112 self.bytes.get(self.pos).copied()
113 }
114
115 fn bump(&mut self) {
116 self.pos += 1;
117 }
118
119 fn at_eof(&self) -> bool {
120 self.pos >= self.bytes.len()
121 }
122
123 fn skip_inline_ws(&mut self) {
124 while matches!(self.peek(), Some(b' ' | b'\t')) {
125 self.bump();
126 }
127 }
128
129 fn skip_trivia(&mut self) {
131 loop {
132 match self.peek() {
133 Some(b' ' | b'\t' | b'\r' | b'\n') => self.bump(),
134 Some(b';') => self.skip_to_eol(),
135 _ => break,
136 }
137 }
138 }
139
140 fn skip_to_eol(&mut self) {
141 while !matches!(self.peek(), None | Some(b'\n')) {
142 self.bump();
143 }
144 if self.peek() == Some(b'\n') {
145 self.bump();
146 }
147 }
148
149 fn read_ident(&mut self) -> Option<SmolStr> {
151 let start = self.pos;
152 while matches!(self.peek(), Some(b) if b.is_ascii_alphanumeric() || b == b'_' || b == b'/')
153 {
154 self.bump();
155 }
156 if self.pos == start {
157 None
158 } else {
159 self.src.get(start..self.pos).map(SmolStr::new)
160 }
161 }
162
163 fn consume_value(&mut self) -> Span {
168 self.skip_inline_ws();
169 let start = self.pos;
170 match self.peek() {
171 Some(b'"') => self.consume_quoted(),
172 Some(b'&' | b'@') => {
173 self.bump();
174 if self.peek() == Some(b'"') {
175 self.consume_quoted();
176 } else {
177 self.consume_bare();
178 }
179 }
180 Some(b'[' | b'{' | b'(') => self.consume_balanced(),
181 Some(b'#') => self.consume_color(),
182 Some(_) => {
183 self.consume_bare();
184 while matches!(self.peek(), Some(b'(' | b'[')) {
186 self.consume_balanced();
187 }
188 }
189 None => {}
190 }
191 (start, self.pos)
192 }
193
194 fn consume_quoted(&mut self) {
196 self.bump(); loop {
198 match self.peek() {
199 None => break,
200 Some(b'\\') => {
201 self.bump();
202 self.bump(); }
204 Some(b'"') => {
205 self.bump();
206 break;
207 }
208 Some(_) => self.bump(),
209 }
210 }
211 }
212
213 fn consume_balanced(&mut self) {
216 let mut depth: u32 = 0;
217 loop {
218 match self.peek() {
219 None => break,
220 Some(b'"') => self.consume_quoted(),
221 Some(b'#') => self.consume_color(), Some(b';') => self.skip_to_eol(),
223 Some(b'(' | b'[' | b'{') => {
224 depth += 1;
225 self.bump();
226 }
227 Some(b')' | b']' | b'}') => {
228 self.bump();
229 depth = depth.saturating_sub(1);
230 if depth == 0 {
231 break;
232 }
233 }
234 Some(_) => self.bump(),
235 }
236 }
237 }
238
239 fn consume_color(&mut self) {
241 self.bump(); while matches!(self.peek(), Some(b) if b.is_ascii_hexdigit()) {
243 self.bump();
244 }
245 }
246
247 fn consume_bare(&mut self) {
249 while matches!(
250 self.peek(),
251 Some(b) if b.is_ascii_alphanumeric() || matches!(b, b'_' | b'+' | b'-' | b'.')
252 ) {
253 self.bump();
254 }
255 }
256
257 fn read_header(&mut self) -> (Option<SmolStr>, HeaderAttrs, bool) {
262 self.bump(); self.skip_inline_ws();
264 let tag = self.read_ident();
265 let mut attrs = HeaderAttrs::default();
266 let mut closed = false;
267 loop {
268 self.skip_inline_ws();
269 match self.peek() {
270 Some(b']') => {
271 self.bump();
272 closed = true;
273 break;
274 }
275 None | Some(b'\n') => break,
277 Some(_) => {
278 let Some(key) = self.read_ident() else {
279 self.bump(); continue;
281 };
282 self.skip_inline_ws();
283 if self.peek() != Some(b'=') {
284 continue; }
286 self.bump(); let value = self.consume_value();
288 attrs.set(&key, value);
289 }
290 }
291 }
292 (tag, attrs, closed)
293 }
294
295 fn consume_body(&mut self, is_node: bool) -> (Option<ExtId>, bool, Vec<NodeProp>) {
299 let mut script = None;
300 let mut unique = false;
301 let mut props = Vec::new();
302 loop {
303 self.skip_trivia();
304 match self.peek() {
305 None | Some(b'[') => break, Some(_) => {}
307 }
308 let key_start = self.pos;
309 let Some(key) = self.read_ident() else {
310 self.skip_to_eol(); continue;
312 };
313 let key_span = TextRange::new(to_u32(key_start), to_u32(self.pos));
314 self.skip_inline_ws();
315 if self.peek() != Some(b'=') {
316 self.skip_to_eol();
317 continue;
318 }
319 self.bump(); let (vs, ve) = self.consume_value();
321 if is_node {
322 match key.as_str() {
323 "script" => script = self.extract_ext_id(vs, ve),
324 "unique_name_in_owner" => {
325 unique = self.src.get(vs..ve).is_some_and(|v| v.trim() == "true");
326 }
327 _ => {}
328 }
329 props.push(NodeProp { key, key_span });
330 }
331 self.skip_to_eol();
332 }
333 (script, unique, props)
334 }
335
336 fn extract_string(&self, span: Span) -> Option<SmolStr> {
341 let raw = self.src.get(span.0..span.1)?.trim();
342 if raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"') {
343 Some(SmolStr::new(unescape(&raw[1..raw.len() - 1])))
344 } else if raw.is_empty() {
345 None
346 } else {
347 Some(SmolStr::new(raw))
348 }
349 }
350
351 fn extract_u8(&self, span: Span) -> Option<u8> {
353 self.extract_string(span)?.trim().parse().ok()
354 }
355
356 fn extract_ext_id(&self, start: usize, end: usize) -> Option<ExtId> {
361 let v = self.src.get(start..end)?;
362 let open = v.find('(')?;
363 if v.get(..open)?.trim() != "ExtResource" {
364 return None;
365 }
366 let close = v.rfind(')')?;
367 if close <= open {
368 return None;
369 }
370 let inner = v.get(open + 1..close)?.trim().trim_matches('"').trim();
371 (!inner.is_empty()).then(|| ExtId(SmolStr::new(inner)))
372 }
373
374 fn run(&mut self) {
377 loop {
378 self.skip_trivia();
379 if self.at_eof() {
380 break;
381 }
382 if self.peek() == Some(b'[') {
383 self.section();
384 } else {
385 self.skip_to_eol(); }
387 }
388 }
389
390 fn section(&mut self) {
391 let start = self.pos;
392 let (tag, attrs, closed) = self.read_header();
393 let header_span = TextRange::new(to_u32(start), to_u32(self.pos));
394 if !closed {
395 self.model
396 .problems
397 .push(SceneProblem::MalformedHeader { at: header_span });
398 }
400 match tag.as_deref() {
401 Some("gd_scene") => {
402 self.model.kind = SceneKind::Scene;
403 self.read_scene_header(&attrs);
404 self.consume_body(false);
405 }
406 Some("gd_resource") => {
407 self.model.kind = SceneKind::Resource;
408 self.read_resource_header(&attrs);
409 self.consume_body(false);
410 }
411 Some("ext_resource") => {
412 self.add_ext_resource(&attrs, header_span);
413 self.consume_body(false);
414 }
415 Some("sub_resource") => {
416 self.add_sub_resource(&attrs, header_span);
417 self.consume_body(false);
418 }
419 Some("node") => self.add_node(&attrs, header_span),
420 Some("connection") => {
421 self.add_connection(&attrs, header_span);
422 self.consume_body(false); }
424 Some("editable" | "resource") => {
425 self.consume_body(false); }
427 Some(_) => {
428 self.model
429 .problems
430 .push(SceneProblem::UnknownTag { at: header_span });
431 self.consume_body(false);
432 }
433 None => {
434 self.model
435 .problems
436 .push(SceneProblem::MalformedHeader { at: header_span });
437 self.consume_body(false);
438 }
439 }
440 }
441
442 fn read_scene_header(&mut self, a: &HeaderAttrs) {
443 self.model.format = a.format.and_then(|s| self.extract_u8(s));
444 self.model.uid = a.uid.and_then(|s| self.extract_string(s));
445 self.model.script_class = a.script_class.and_then(|s| self.extract_string(s));
446 }
447
448 fn read_resource_header(&mut self, a: &HeaderAttrs) {
449 self.model.format = a.format.and_then(|s| self.extract_u8(s));
450 self.model.uid = a.uid.and_then(|s| self.extract_string(s));
451 self.model.script_class = a.script_class.and_then(|s| self.extract_string(s));
452 self.model.resource_type = a.typ.and_then(|s| self.extract_string(s));
453 }
454
455 fn add_ext_resource(&mut self, a: &HeaderAttrs, span: TextRange) {
456 let res_type = a.typ.and_then(|s| self.extract_string(s));
457 let path = a.path.and_then(|s| self.extract_string(s));
458 let uid = a.uid.and_then(|s| self.extract_string(s));
459 let id = a.id.and_then(|s| self.extract_string(s));
460 match id {
461 Some(id) => {
462 if res_type.is_none() || path.is_none() {
463 self.model
464 .problems
465 .push(SceneProblem::MissingExtField { at: span });
466 }
467 self.model.ext_resources.insert(
468 ExtId(id),
469 ExtResource {
470 res_type: res_type.unwrap_or_default(),
471 path,
472 uid,
473 span,
474 },
475 );
476 }
477 None => self
478 .model
479 .problems
480 .push(SceneProblem::MissingExtField { at: span }),
481 }
482 }
483
484 fn add_sub_resource(&mut self, a: &HeaderAttrs, span: TextRange) {
485 let res_type = a
486 .typ
487 .and_then(|s| self.extract_string(s))
488 .unwrap_or_default();
489 if let Some(id) = a.id.and_then(|s| self.extract_string(s)) {
490 self.model
491 .sub_resources
492 .insert(ExtId(id), SubResource { res_type, span });
493 }
494 }
495
496 fn add_node(&mut self, a: &HeaderAttrs, header_span: TextRange) {
497 let name = a
498 .name
499 .and_then(|s| self.extract_string(s))
500 .unwrap_or_default();
501 let name_span = a.name.map_or(header_span, |sp| self.inner_span(sp));
504 let decl_type = a.typ.and_then(|s| self.extract_string(s));
505 let parent_path = a.parent.and_then(|s| self.extract_string(s));
506 let parent_span = a.parent.map(|sp| self.inner_span(sp));
507 let instance = a.instance.and_then(|(s, e)| self.extract_ext_id(s, e));
508 let instance_placeholder = a.instance_placeholder.is_some();
509 let (script, unique_name_in_owner, properties) = self.consume_body(true);
510 self.model.nodes.push(SceneNode {
511 name,
512 decl_type,
513 parent_path,
514 parent_span,
515 parent_idx: None,
516 script,
517 instance,
518 instance_is_inherited_root: false,
519 instance_placeholder,
520 unique_name_in_owner,
521 header_span,
522 name_span,
523 properties,
524 });
525 }
526
527 fn add_connection(&mut self, a: &HeaderAttrs, header_span: TextRange) {
528 let value = |s: Option<Span>| s.and_then(|sp| self.extract_string(sp)).unwrap_or_default();
532 let span = |s: Option<Span>| s.map_or(header_span, |sp| self.inner_span(sp));
533 self.model.connections.push(SceneConnection {
534 signal: value(a.signal),
535 signal_span: span(a.signal),
536 from: value(a.from),
537 from_span: span(a.from),
538 to: value(a.to),
539 to_span: span(a.to),
540 method: value(a.method),
541 method_span: span(a.method),
542 header_span,
543 });
544 }
545
546 fn inner_span(&self, (s, e): Span) -> TextRange {
551 let raw = self.src.get(s..e).unwrap_or("");
552 let lead = raw.len() - raw.trim_start().len();
553 let trail = raw.len() - raw.trim_end().len();
554 let mut lo = s + lead;
555 let mut hi = e - trail;
556 let trimmed = &raw[lead..raw.len() - trail];
557 if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') {
558 lo += 1;
559 hi -= 1;
560 }
561 TextRange::new(to_u32(lo), to_u32(hi))
562 }
563
564 fn build_tree(&mut self) {
567 let n = self.model.nodes.len();
568 if n == 0 {
569 return;
570 }
571 let roots: Vec<NodeIdx> = (0..n)
573 .filter(|&i| self.model.nodes[i].parent_path.is_none())
574 .map(|i| NodeIdx(to_u32(i)))
575 .collect();
576 self.model.root = roots.first().copied();
577 if roots.len() > 1 {
578 self.model.problems.push(SceneProblem::MultipleRoots {
579 roots: roots.clone(),
580 });
581 } else if roots.is_empty() {
582 self.model.problems.push(SceneProblem::NoRoot);
583 }
584 let root = self.model.root;
585
586 let mut child_index: FxHashMap<(NodeIdx, SmolStr), NodeIdx> = FxHashMap::default();
589 let mut children: FxHashMap<NodeIdx, Vec<NodeIdx>> = FxHashMap::default();
590 let mut full_paths: Vec<SmolStr> = vec![SmolStr::default(); n];
591 let mut dangling_subtrees: FxHashSet<SmolStr> = FxHashSet::default();
595
596 for i in 0..n {
597 let idx = NodeIdx(to_u32(i));
598 let parent_path = self.model.nodes[i].parent_path.clone();
599 let name = self.model.nodes[i].name.clone();
600
601 if Some(idx) == root && self.model.nodes[i].instance.is_some() {
605 self.model.nodes[i].instance_is_inherited_root = true;
606 }
607
608 let parent_idx = match parent_path.as_deref() {
609 None => None,
610 Some(".") => root,
611 Some(p) => match walk_path(root, p, &child_index) {
612 Walk::Resolved(found) => Some(found),
613 Walk::Escaped => None,
616 Walk::Missed(deepest) => {
617 let is_cascade = within_dangling_subtree(p, &dangling_subtrees);
621 dangling_subtrees.insert(SmolStr::new(format!("{p}/{name}")));
622 if !is_cascade && !self.model.descends_from_instance(deepest) {
629 self.model.problems.push(SceneProblem::DanglingParent {
630 node: idx,
631 parent_path: SmolStr::new(p),
632 });
633 }
634 None
635 }
636 },
637 };
638 self.model.nodes[i].parent_idx = parent_idx;
639
640 if let Some(p) = parent_idx {
641 child_index.entry((p, name.clone())).or_insert(idx);
644 children.entry(p).or_default().push(idx);
645 let pfp = &full_paths[p.0 as usize];
646 let fp = if pfp.is_empty() {
647 name
648 } else {
649 SmolStr::new(format!("{pfp}/{name}"))
650 };
651 full_paths[i] = fp.clone();
652 self.model.by_path.entry(fp).or_insert(idx);
653 }
654 }
655
656 for i in 0..n {
658 if self.model.nodes[i].unique_name_in_owner {
659 self.model
660 .unique_nodes
661 .entry(self.model.nodes[i].name.clone())
662 .or_insert(NodeIdx(to_u32(i)));
663 }
664 }
665
666 for i in 0..n {
668 let span = self.model.nodes[i].header_span;
669 let refs = [
670 self.model.nodes[i].script.clone(),
671 self.model.nodes[i].instance.clone(),
672 ];
673 for id in refs.into_iter().flatten() {
674 if !self.model.ext_resources.contains_key(&id) {
675 self.model
676 .problems
677 .push(SceneProblem::UnknownExtResource { id, at: span });
678 }
679 }
680 }
681
682 self.model.set_indices(child_index, children);
683 }
684}
685
686fn within_dangling_subtree(p: &str, dangling: &FxHashSet<SmolStr>) -> bool {
690 dangling.iter().any(|d| {
691 p == d.as_str()
692 || p.strip_prefix(d.as_str())
693 .is_some_and(|rest| rest.starts_with('/'))
694 })
695}
696
697enum Walk {
699 Resolved(NodeIdx),
701 Escaped,
704 Missed(Option<NodeIdx>),
707}
708
709fn walk_path(
712 root: Option<NodeIdx>,
713 path: &str,
714 child_index: &FxHashMap<(NodeIdx, SmolStr), NodeIdx>,
715) -> Walk {
716 if path.starts_with('/') {
717 return Walk::Escaped; }
719 let Some(mut cur) = root else {
720 return Walk::Missed(None);
721 };
722 for seg in path.split('/') {
723 if seg.is_empty() || seg == "." {
724 continue;
725 }
726 if seg == ".." {
727 return Walk::Escaped; }
729 match child_index.get(&(cur, SmolStr::new(seg))) {
730 Some(&next) => cur = next,
731 None => return Walk::Missed(Some(cur)),
732 }
733 }
734 Walk::Resolved(cur)
735}
736
737fn unescape(s: &str) -> String {
742 if !s.contains('\\') {
743 return s.to_owned();
744 }
745 let chars: Vec<char> = s.chars().collect();
747 let mut out = String::with_capacity(s.len());
748 let mut i = 0;
749 while i < chars.len() {
750 let c = chars[i];
751 if c != '\\' {
752 out.push(c);
753 i += 1;
754 continue;
755 }
756 i += 1; let Some(&esc) = chars.get(i) else {
758 out.push('\\'); break;
760 };
761 i += 1; match esc {
763 'n' => out.push('\n'),
764 't' => out.push('\t'),
765 'r' => out.push('\r'),
766 'a' => out.push('\u{07}'),
767 'b' => out.push('\u{08}'),
768 'f' => out.push('\u{0C}'),
769 'v' => out.push('\u{0B}'),
770 'u' => i = push_unicode_escape(&mut out, &chars, i, 4),
773 'U' => i = push_unicode_escape(&mut out, &chars, i, 6),
774 other => out.push(other), }
776 }
777 out
778}
779
780fn push_unicode_escape(out: &mut String, chars: &[char], start: usize, max_digits: usize) -> usize {
785 let (code, i) = read_hex(chars, start, max_digits);
786 let Some(code) = code else { return start };
787 if (0xD800..=0xDBFF).contains(&code)
789 && chars.get(i) == Some(&'\\')
790 && chars.get(i + 1) == Some(&'u')
791 {
792 let (low, j) = read_hex(chars, i + 2, 4);
793 if let Some(low) = low
794 && (0xDC00..=0xDFFF).contains(&low)
795 {
796 let combined = 0x1_0000 + ((code - 0xD800) << 10) + (low - 0xDC00);
797 if let Some(c) = char::from_u32(combined) {
798 out.push(c);
799 return j;
800 }
801 }
802 }
803 out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
804 i
805}
806
807fn read_hex(chars: &[char], start: usize, max_digits: usize) -> (Option<u32>, usize) {
810 let mut value: u32 = 0;
811 let mut count = 0;
812 let mut i = start;
813 while count < max_digits {
814 let Some(d) = chars.get(i).and_then(|c| c.to_digit(16)) else {
815 break;
816 };
817 value = value * 16 + d;
818 i += 1;
819 count += 1;
820 }
821 if count == 0 {
822 (None, start)
823 } else {
824 (Some(value), i)
825 }
826}
827
828fn to_u32(v: usize) -> u32 {
830 u32::try_from(v).unwrap_or(u32::MAX)
831}
832
833#[cfg(test)]
834mod unescape_tests {
835 use super::unescape;
836
837 #[test]
838 fn passes_plain_strings_and_simple_escapes() {
839 assert_eq!(unescape("Node"), "Node");
840 assert_eq!(unescape("a\\nb"), "a\nb");
841 assert_eq!(unescape("tab\\there"), "tab\there");
842 assert_eq!(unescape("quote\\\"end"), "quote\"end");
843 assert_eq!(unescape("back\\\\slash"), "back\\slash");
844 }
845
846 #[test]
847 fn decodes_control_escapes() {
848 assert_eq!(unescape("\\b"), "\u{08}");
849 assert_eq!(unescape("\\f"), "\u{0C}");
850 assert_eq!(unescape("\\a"), "\u{07}");
851 assert_eq!(unescape("\\v"), "\u{0B}");
852 }
853
854 #[test]
855 fn decodes_lowercase_u_4_hex() {
856 assert_eq!(unescape("\\u0041"), "A");
858 assert_eq!(unescape("X\\u00e9Y"), "XéY");
859 }
860
861 #[test]
862 fn decodes_uppercase_u_6_hex() {
863 assert_eq!(unescape("\\U01F600"), "😀");
865 }
866
867 #[test]
868 fn combines_a_utf16_surrogate_pair() {
869 assert_eq!(unescape("\\uD83D\\uDE00"), "😀");
871 }
872
873 #[test]
874 fn invalid_or_empty_escapes_are_safe() {
875 assert_eq!(unescape("ends\\"), "ends\\");
877 assert_eq!(unescape("\\uZ"), "Z");
879 assert_eq!(unescape("\\uD83D!"), "\u{FFFD}!");
881 }
882}