1use super::make_file_node;
2use crate::error::Result;
3use crate::extract::make_id;
4use crate::types::{Edge, ExtractionFragment, Node};
5use std::collections::{HashMap, HashSet};
6use std::path::Path;
7
8pub struct DmExtractor;
11
12impl DmExtractor {
13 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
14 let (file_id, _file_label, file_node) = make_file_node(path);
15 let str_path = path.to_string_lossy().to_string();
16
17 let mut nodes: Vec<Node> = vec![file_node];
18 let mut edges: Vec<Edge> = vec![];
19 let mut seen_ids: HashSet<String> = HashSet::new();
20 seen_ids.insert(file_id.clone());
21
22 let text = std::str::from_utf8(source).unwrap_or("");
23 let stem = file_id.clone();
24
25 let mut current_type: Option<(String, String)> = None;
27 let mut current_proc: Option<String> = None;
29 let mut proc_body_min_indent: usize = 0;
31
32 let mut path_to_nid: HashMap<String, String> = HashMap::new();
34 let mut name_to_nids: HashMap<String, Vec<String>> = HashMap::new();
36
37 let mut pending_calls: Vec<(String, String, usize)> = vec![];
39 let mut pending_news: Vec<(String, String, usize)> = vec![];
41
42 let add_node =
43 |nodes: &mut Vec<Node>, seen: &mut HashSet<String>, id: String, label: String| {
44 if seen.insert(id.clone()) {
45 nodes.push(Node {
46 id,
47 label,
48 file_type: "code".to_string(),
49 source_file: str_path.clone(),
50 source_location: None,
51 community: None,
52 rationale: None,
53 docstring: None,
54 metadata: HashMap::new(),
55 });
56 }
57 };
58
59 let make_edge = |src: &str, tgt: &str, relation: &str, context: Option<&str>| Edge {
60 source: src.to_string(),
61 target: tgt.to_string(),
62 relation: relation.to_string(),
63 confidence: "EXTRACTED".to_string(),
64 source_file: Some(str_path.clone()),
65 weight: 1.0,
66 context: context.map(|s| s.to_string()),
67 };
68
69 let indent_of = |line: &str| line.chars().take_while(|&c| c == '\t').count();
71
72 for (line_idx, raw_line) in text.lines().enumerate() {
73 let tabs = indent_of(raw_line);
74 let trimmed = raw_line.trim_start_matches('\t');
75
76 if trimmed.is_empty() || trimmed.starts_with("//") {
77 continue;
78 }
79
80 if current_proc.is_some() && tabs < proc_body_min_indent {
82 current_proc = None;
83 }
84
85 if tabs == 0 {
86 current_type = None;
88 current_proc = None;
89
90 if trimmed.starts_with("#include") {
91 let raw_inc = trimmed
93 .trim_start_matches("#include")
94 .trim()
95 .trim_matches(|c| c == '"' || c == '\'' || c == '<' || c == '>');
96 if !raw_inc.is_empty() {
97 let norm = raw_inc.replace('\\', "/");
98 let target_file = path.parent().and_then(|p| {
99 let candidate = p.join(&norm);
100 if candidate.exists() {
101 Some(candidate)
102 } else {
103 None
104 }
105 });
106 let (tgt_id, relation) = match &target_file {
107 Some(resolved) => (
108 make_id(&[resolved.to_string_lossy().as_ref()]),
109 "imports_from",
110 ),
111 None => {
112 let base = norm
113 .rsplit('/')
114 .next()
115 .unwrap_or(&norm)
116 .trim_end_matches(".dm");
117 (make_id(&[base]), "imports")
118 }
119 };
120 edges.push(make_edge(&file_id, &tgt_id, relation, Some("import")));
121 }
122 continue;
123 }
124
125 if trimmed.starts_with("var/") {
126 continue; }
128
129 if !trimmed.starts_with('/') {
130 continue;
131 }
132
133 if let Some(paren_pos) = trimmed.find('(') {
135 let full_path = &trimmed[..paren_pos];
136 let components: Vec<&str> =
137 full_path.split('/').filter(|s| !s.is_empty()).collect();
138
139 if components.is_empty() {
140 continue;
141 }
142
143 if components[0] == "proc" {
144 let proc_name = if components.len() >= 2 {
146 components[1]
147 } else {
148 continue;
149 };
150 let label = format!("{}()", proc_name);
151 let nid = make_id(&[&stem, proc_name]);
152 add_node(&mut nodes, &mut seen_ids, nid.clone(), label.clone());
153 edges.push(make_edge(&file_id, &nid, "contains", None));
154 let simple = proc_name.to_lowercase();
155 name_to_nids.entry(simple).or_default().push(nid.clone());
156 current_proc = Some(nid);
157 proc_body_min_indent = 1;
158 } else if components.len() >= 2 && components[components.len() - 2] == "proc" {
159 let type_components = &components[..components.len() - 2];
161 let proc_name = components[components.len() - 1];
162 let type_path = format!("/{}", type_components.join("/"));
163 let type_id_part = type_components.join("_");
164 let type_nid = make_id(&[&stem, &type_id_part]);
165 if seen_ids.insert(type_nid.clone()) {
167 nodes.push(Node {
168 id: type_nid.clone(),
169 label: type_path.clone(),
170 file_type: "code".to_string(),
171 source_file: str_path.clone(),
172 source_location: None,
173 community: None,
174 rationale: None,
175 docstring: None,
176 metadata: HashMap::new(),
177 });
178 edges.push(make_edge(&file_id, &type_nid, "contains", None));
179 path_to_nid.insert(type_path.clone(), type_nid.clone());
180 }
181 let proc_label = format!("{}/{}()", type_path, proc_name);
182 let proc_id_part = format!("{}_{}", type_id_part, proc_name);
183 let proc_nid = make_id(&[&stem, &proc_id_part]);
184 add_node(
185 &mut nodes,
186 &mut seen_ids,
187 proc_nid.clone(),
188 proc_label.clone(),
189 );
190 edges.push(make_edge(&type_nid, &proc_nid, "method", None));
191 let simple = proc_name.to_lowercase();
192 name_to_nids
193 .entry(simple)
194 .or_default()
195 .push(proc_nid.clone());
196 current_proc = Some(proc_nid);
197 proc_body_min_indent = 1;
198 } else {
199 let type_components = &components[..components.len() - 1];
201 let proc_name = components[components.len() - 1];
202 let type_path = if type_components.is_empty() {
203 String::new()
204 } else {
205 format!("/{}", type_components.join("/"))
206 };
207 let proc_label = if type_path.is_empty() {
208 format!("{}()", proc_name)
209 } else {
210 format!("{}/{}()", type_path, proc_name)
211 };
212 let type_id_part = type_components.join("_");
213 let proc_id_part = if type_id_part.is_empty() {
214 proc_name.to_string()
215 } else {
216 format!("{}_{}", type_id_part, proc_name)
217 };
218 let proc_nid = make_id(&[&stem, &proc_id_part]);
219 if !type_path.is_empty() {
221 let type_nid = make_id(&[&stem, &type_id_part]);
222 if seen_ids.insert(type_nid.clone()) {
223 nodes.push(Node {
224 id: type_nid.clone(),
225 label: type_path.clone(),
226 file_type: "code".to_string(),
227 source_file: str_path.clone(),
228 source_location: None,
229 community: None,
230 rationale: None,
231 docstring: None,
232 metadata: HashMap::new(),
233 });
234 edges.push(make_edge(&file_id, &type_nid, "contains", None));
235 path_to_nid.insert(type_path.clone(), type_nid.clone());
236 }
237 let type_nid = make_id(&[&stem, &type_id_part]);
238 add_node(&mut nodes, &mut seen_ids, proc_nid.clone(), proc_label);
239 edges.push(make_edge(&type_nid, &proc_nid, "method", None));
240 } else {
241 add_node(&mut nodes, &mut seen_ids, proc_nid.clone(), proc_label);
242 edges.push(make_edge(&file_id, &proc_nid, "contains", None));
243 }
244 let simple = proc_name.to_lowercase();
245 name_to_nids
246 .entry(simple)
247 .or_default()
248 .push(proc_nid.clone());
249 current_proc = Some(proc_nid);
250 proc_body_min_indent = 1;
251 }
252 } else {
253 let type_path = trimmed.trim().to_string();
255 let components: Vec<&str> =
256 type_path.split('/').filter(|s| !s.is_empty()).collect();
257 let type_id_part = components.join("_");
258 let type_nid = make_id(&[&stem, &type_id_part]);
259 if seen_ids.insert(type_nid.clone()) {
260 nodes.push(Node {
261 id: type_nid.clone(),
262 label: type_path.clone(),
263 file_type: "code".to_string(),
264 source_file: str_path.clone(),
265 source_location: None,
266 community: None,
267 rationale: None,
268 docstring: None,
269 metadata: HashMap::new(),
270 });
271 edges.push(make_edge(&file_id, &type_nid, "contains", None));
272 path_to_nid.insert(type_path.clone(), type_nid.clone());
273 }
274 current_type = Some((type_path, type_nid));
275 }
276 } else if tabs == 1 {
277 if let Some((ref type_path, ref type_nid)) = current_type.clone() {
278 if trimmed.starts_with("var/") || trimmed.starts_with("//") {
280 } else if let Some(paren_pos) = trimmed.find('(') {
282 let before_paren = &trimmed[..paren_pos];
283 let proc_name = if let Some(stripped) = before_paren.strip_prefix("proc/") {
285 stripped.trim()
286 } else {
287 before_paren.trim()
289 };
290 if !proc_name.is_empty()
291 && proc_name.chars().all(|c| c.is_alphanumeric() || c == '_')
292 {
293 let proc_label = format!("{}/{}()", type_path, proc_name);
294 let type_id_part = type_path
295 .split('/')
296 .filter(|s| !s.is_empty())
297 .collect::<Vec<_>>()
298 .join("_");
299 let proc_id_part = format!("{}_{}", type_id_part, proc_name);
300 let proc_nid = make_id(&[&stem, &proc_id_part]);
301 add_node(&mut nodes, &mut seen_ids, proc_nid.clone(), proc_label);
302 edges.push(make_edge(type_nid, &proc_nid, "method", None));
303 let simple = proc_name.to_lowercase();
304 name_to_nids
305 .entry(simple)
306 .or_default()
307 .push(proc_nid.clone());
308 current_proc = Some(proc_nid);
309 proc_body_min_indent = 2;
310 }
311 } else {
312 current_proc = None;
314 }
315 } else if let Some(ref caller_nid) = current_proc.clone() {
316 if proc_body_min_indent <= 1 {
317 collect_calls(
319 trimmed,
320 caller_nid,
321 &mut pending_calls,
322 &mut pending_news,
323 line_idx,
324 );
325 }
326 }
327 } else {
328 if let Some(ref caller_nid) = current_proc.clone() {
330 if tabs >= proc_body_min_indent {
331 collect_calls(
332 trimmed,
333 caller_nid,
334 &mut pending_calls,
335 &mut pending_news,
336 line_idx,
337 );
338 }
339 }
340 }
341 }
342
343 let mut seen_call_pairs: HashSet<(String, String)> = HashSet::new();
345 for (caller_nid, callee_name, _line) in &pending_calls {
346 if callee_name == ".." {
347 continue;
348 }
349 let lower = callee_name.to_lowercase();
350 if let Some(candidates) = name_to_nids.get(&lower) {
351 if candidates.len() == 1 {
352 let tgt = &candidates[0];
353 if tgt != caller_nid {
354 let pair = (caller_nid.clone(), tgt.clone());
355 if seen_call_pairs.insert(pair) {
356 edges.push(Edge {
357 source: caller_nid.clone(),
358 target: tgt.clone(),
359 relation: "calls".to_string(),
360 confidence: "EXTRACTED".to_string(),
361 source_file: Some(str_path.clone()),
362 weight: 1.0,
363 context: Some("call".to_string()),
364 });
365 }
366 }
367 }
368 }
370 }
372
373 for (caller_nid, type_path, _line) in &pending_news {
375 if let Some(tgt_nid) = path_to_nid.get(type_path) {
376 if tgt_nid != caller_nid {
377 let pair = (caller_nid.clone(), tgt_nid.clone());
378 if seen_call_pairs.insert(pair) {
379 edges.push(Edge {
380 source: caller_nid.clone(),
381 target: tgt_nid.clone(),
382 relation: "instantiates".to_string(),
383 confidence: "EXTRACTED".to_string(),
384 source_file: Some(str_path.clone()),
385 weight: 1.0,
386 context: Some("call".to_string()),
387 });
388 }
389 }
390 }
391 }
392
393 Ok(ExtractionFragment { nodes, edges })
394 }
395}
396
397fn collect_calls(
398 line: &str,
399 caller_nid: &str,
400 pending_calls: &mut Vec<(String, String, usize)>,
401 pending_news: &mut Vec<(String, String, usize)>,
402 line_idx: usize,
403) {
404 let mut rest = line;
406 while let Some(pos) = rest.find("new ") {
407 let after = rest[pos + 4..].trim_start();
408 if after.starts_with('/') {
409 let end = after
410 .find(|c: char| !c.is_alphanumeric() && c != '/' && c != '_')
411 .unwrap_or(after.len());
412 let type_path = &after[..end];
413 if !type_path.is_empty() {
414 pending_news.push((caller_nid.to_string(), type_path.to_string(), line_idx));
415 }
416 }
417 rest = &rest[pos + 4..];
418 }
419
420 let mut chars = line.char_indices().peekable();
422 while let Some((i, c)) = chars.next() {
423 if c.is_alphabetic() || c == '_' {
424 let start = i;
426 let mut end = i + c.len_utf8();
427 while let Some(&(j, nc)) = chars.peek() {
428 if nc.is_alphanumeric() || nc == '_' {
429 end = j + nc.len_utf8();
430 chars.next();
431 } else {
432 break;
433 }
434 }
435 if let Some(&(_, '(')) = chars.peek() {
437 let ident = &line[start..end];
438 if !is_dm_keyword(ident) && ident != ".." {
440 let preceded_by_dot =
442 start > 0 && line.as_bytes().get(start - 1) == Some(&b'.');
443 let _ = preceded_by_dot; pending_calls.push((caller_nid.to_string(), ident.to_string(), line_idx));
445 }
446 }
447 }
448 }
449}
450
451fn is_dm_keyword(s: &str) -> bool {
452 matches!(
453 s,
454 "if" | "else"
455 | "for"
456 | "while"
457 | "switch"
458 | "return"
459 | "var"
460 | "spawn"
461 | "new"
462 | "del"
463 | "null"
464 | "TRUE"
465 | "FALSE"
466 | "src"
467 | "usr"
468 | "world"
469 | "global"
470 | "proc"
471 | "verb"
472 | "list"
473 | "datum"
474 | "atom"
475 | "mob"
476 | "obj"
477 | "turf"
478 | "area"
479 )
480}
481
482pub struct DmiExtractor;
485
486impl DmiExtractor {
487 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
488 let (file_id, _file_label, file_node) = make_file_node(path);
489 let str_path = path.to_string_lossy().to_string();
490 let stem = file_id.clone();
491
492 let mut nodes: Vec<Node> = vec![file_node];
493 let mut edges: Vec<Edge> = vec![];
494 let mut seen: HashSet<String> = HashSet::new();
495 seen.insert(file_id.clone());
496
497 let description = read_dmi_description(source);
498 for raw_line in description.lines() {
499 let stripped = raw_line.trim();
500 if !stripped.starts_with("state =") {
501 continue;
502 }
503 let value = stripped["state =".len()..].trim();
504 let state_name = if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
505 &value[1..value.len() - 1]
506 } else {
507 value
508 };
509 if state_name.is_empty() {
510 continue;
511 }
512 let nid = make_id(&[&stem, "state", state_name]);
513 if seen.insert(nid.clone()) {
514 nodes.push(Node {
515 id: nid.clone(),
516 label: format!("\"{}\"", state_name),
517 file_type: "code".to_string(),
518 source_file: str_path.clone(),
519 source_location: None,
520 community: None,
521 rationale: None,
522 docstring: None,
523 metadata: HashMap::new(),
524 });
525 edges.push(Edge {
526 source: file_id.clone(),
527 target: nid,
528 relation: "contains".to_string(),
529 confidence: "EXTRACTED".to_string(),
530 source_file: Some(str_path.clone()),
531 weight: 1.0,
532 context: None,
533 });
534 }
535 }
536
537 Ok(ExtractionFragment { nodes, edges })
538 }
539}
540
541fn read_dmi_description(data: &[u8]) -> String {
542 const PNG_HEADER: &[u8] = b"\x89PNG\r\n\x1a\n";
543 if !data.starts_with(PNG_HEADER) {
544 return String::new();
545 }
546 let mut i = 8usize;
547 while i + 8 <= data.len() {
548 let length = u32::from_be_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]) as usize;
549 let chunk_type = &data[i + 4..i + 8];
550 let payload_start = i + 8;
551 let payload_end = payload_start + length;
552 if payload_end > data.len() {
553 break;
554 }
555 let payload = &data[payload_start..payload_end];
556
557 if chunk_type == b"tEXt" {
558 if let Some(null_pos) = payload.iter().position(|&b| b == 0) {
559 let keyword = &payload[..null_pos];
560 if keyword == b"Description" {
561 let text = &payload[null_pos + 1..];
562 return String::from_utf8_lossy(text).to_string();
563 }
564 }
565 }
566 i += 8 + length + 4; }
569 String::new()
570}
571
572pub struct DmmExtractor;
575
576impl DmmExtractor {
577 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
578 let (file_id, _file_label, file_node) = make_file_node(path);
579 let str_path = path.to_string_lossy().to_string();
580
581 let nodes: Vec<Node> = vec![file_node];
582 let mut edges: Vec<Edge> = vec![];
583
584 let text = std::str::from_utf8(source).unwrap_or("");
585
586 let dict_text = if let Some(grid_pos) = find_grid_section(text) {
588 &text[..grid_pos]
589 } else {
590 text
591 };
592
593 let mut seen_targets: HashSet<String> = HashSet::new();
594
595 for type_path in extract_type_paths(dict_text) {
598 let tgt = make_id(&[&type_path]);
599 if seen_targets.insert(tgt.clone()) {
600 edges.push(Edge {
601 source: file_id.clone(),
602 target: tgt,
603 relation: "uses".to_string(),
604 confidence: "EXTRACTED".to_string(),
605 source_file: Some(str_path.clone()),
606 weight: 1.0,
607 context: Some("map".to_string()),
608 });
609 }
610 }
611
612 Ok(ExtractionFragment { nodes, edges })
613 }
614}
615
616fn find_grid_section(text: &str) -> Option<usize> {
617 let mut pos = 0;
619 for line in text.lines() {
620 let trimmed = line.trim();
621 if trimmed.starts_with('(') {
622 if let Some(close) = trimmed.find(')') {
624 let inner = &trimmed[1..close];
625 let parts: Vec<&str> = inner.split(',').collect();
626 if parts.len() == 3 && parts.iter().all(|p| p.trim().parse::<u32>().is_ok()) {
627 return Some(pos);
628 }
629 }
630 }
631 pos += line.len() + 1; }
633 None
634}
635
636fn extract_type_paths(dict_text: &str) -> Vec<String> {
637 let mut result = Vec::new();
638 let mut chars = dict_text.char_indices().peekable();
643 while let Some((i, c)) = chars.next() {
644 if c == '/' {
645 let start = i;
647 let mut end = i + 1;
648 while let Some(&(j, nc)) = chars.peek() {
649 if nc.is_alphanumeric() || nc == '_' || nc == '/' {
650 end = j + nc.len_utf8();
651 chars.next();
652 } else {
653 break;
654 }
655 }
656 let raw = &dict_text[start..end];
657 let components: Vec<&str> = raw.split('/').filter(|s| !s.is_empty()).collect();
659 if !components.is_empty() && components.iter().all(|c| !c.is_empty()) {
660 result.push(raw.to_string());
661 }
662 }
663 }
664 result
665}
666
667pub struct DmfExtractor;
670
671impl DmfExtractor {
672 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
673 let (file_id, _file_label, file_node) = make_file_node(path);
674 let str_path = path.to_string_lossy().to_string();
675 let stem = file_id.clone();
676
677 let mut nodes: Vec<Node> = vec![file_node];
678 let mut edges: Vec<Edge> = vec![];
679 let mut seen: HashSet<String> = HashSet::new();
680 seen.insert(file_id.clone());
681
682 let text = std::str::from_utf8(source).unwrap_or("");
683
684 let mut current_window_nid: Option<String> = None;
685 let mut current_elem: Option<(usize, String)> = None;
687
688 for line in text.lines() {
689 let trimmed = line.trim();
690 if trimmed.is_empty() {
691 continue;
692 }
693
694 if let Some(name) = parse_quoted_directive(trimmed, "window") {
696 let nid = make_id(&[&stem, "window", &name]);
697 if seen.insert(nid.clone()) {
698 nodes.push(Node {
699 id: nid.clone(),
700 label: format!("window \"{}\"", name),
701 file_type: "code".to_string(),
702 source_file: str_path.clone(),
703 source_location: None,
704 community: None,
705 rationale: None,
706 docstring: None,
707 metadata: HashMap::new(),
708 });
709 edges.push(Edge {
710 source: file_id.clone(),
711 target: nid.clone(),
712 relation: "contains".to_string(),
713 confidence: "EXTRACTED".to_string(),
714 source_file: Some(str_path.clone()),
715 weight: 1.0,
716 context: None,
717 });
718 }
719 current_window_nid = Some(nid);
720 current_elem = None;
721 continue;
722 }
723
724 if let Some(elem_name) = parse_quoted_directive(trimmed, "elem") {
726 if let Some(ref win_nid) = current_window_nid.clone() {
727 let nid = make_id(&[&stem, "elem", win_nid, &elem_name]);
728 if seen.insert(nid.clone()) {
729 let elem_idx = nodes.len();
730 nodes.push(Node {
731 id: nid.clone(),
732 label: format!("elem \"{}\"", elem_name),
733 file_type: "code".to_string(),
734 source_file: str_path.clone(),
735 source_location: None,
736 community: None,
737 rationale: None,
738 docstring: None,
739 metadata: HashMap::new(),
740 });
741 edges.push(Edge {
742 source: win_nid.clone(),
743 target: nid.clone(),
744 relation: "contains".to_string(),
745 confidence: "EXTRACTED".to_string(),
746 source_file: Some(str_path.clone()),
747 weight: 1.0,
748 context: None,
749 });
750 current_elem = Some((elem_idx, elem_name));
751 }
752 }
753 continue;
754 }
755
756 if let Some((elem_idx, ref elem_name)) = current_elem.clone() {
758 if let Some(type_val) = parse_kv_directive(trimmed, "type") {
759 if !nodes[elem_idx].label.contains('[') {
760 let new_label = format!("elem \"{}\" [{}]", elem_name, type_val);
761 nodes[elem_idx].label = new_label;
762 }
763 }
764 }
765 }
766
767 Ok(ExtractionFragment { nodes, edges })
768 }
769}
770
771fn parse_quoted_directive(line: &str, keyword: &str) -> Option<String> {
772 let prefix = format!("{} \"", keyword);
773 if line.starts_with(&prefix) {
774 let rest = &line[prefix.len()..];
775 if let Some(end) = rest.find('"') {
776 return Some(rest[..end].to_string());
777 }
778 }
779 None
780}
781
782fn parse_kv_directive(line: &str, key: &str) -> Option<String> {
783 let prefix = format!("{} =", key);
784 if let Some(stripped) = line.strip_prefix(&prefix) {
785 return Some(stripped.trim().to_string());
786 }
787 let prefix2 = format!("{}=", key);
788 if let Some(stripped) = line.strip_prefix(&prefix2) {
789 return Some(stripped.trim().to_string());
790 }
791 None
792}