1use std::collections::{HashMap, HashSet};
8use std::fmt;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13use tracing::debug;
14
15use crate::lower::base_modules;
16use crate::scan;
17
18#[allow(dead_code)]
19mod document;
20
21pub use document::{
22 ByteOffset, BytePosition, Position, PositionEncoding, PositionError, SourceDocument, SourceId,
23 SourceOrigin, SourceRange, SourceRangeError, SourceSet,
24};
25
26pub const DEFAULT_EXTENSIONS: &[&str] = &["", ".mib", ".smi", ".txt", ".my"];
30
31#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
38pub struct CandidateId(Arc<str>);
39
40impl CandidateId {
41 pub fn new(identity: impl Into<Arc<str>>) -> Self {
43 Self(identity.into())
44 }
45
46 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50
51 fn scoped(&self, scope: usize) -> Self {
52 Self::new(format!("{scope}:{}:{}", self.0.len(), self.0))
53 }
54}
55
56impl fmt::Display for CandidateId {
57 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58 self.0.fmt(formatter)
59 }
60}
61
62impl From<&str> for CandidateId {
63 fn from(identity: &str) -> Self {
64 Self::new(identity)
65 }
66}
67
68impl From<String> for CandidateId {
69 fn from(identity: String) -> Self {
70 Self::new(identity)
71 }
72}
73
74impl From<Arc<str>> for CandidateId {
75 fn from(identity: Arc<str>) -> Self {
76 Self(identity)
77 }
78}
79
80#[derive(Clone, Debug)]
86pub struct SourceCandidate {
87 identity: CandidateId,
88 origin: SourceOrigin,
89 label: Arc<str>,
90 bytes: Arc<[u8]>,
91}
92
93impl SourceCandidate {
94 pub fn new(
96 identity: impl Into<CandidateId>,
97 origin: SourceOrigin,
98 label: impl Into<Arc<str>>,
99 bytes: impl Into<Arc<[u8]>>,
100 ) -> Self {
101 Self {
102 identity: identity.into(),
103 origin,
104 label: label.into(),
105 bytes: bytes.into(),
106 }
107 }
108
109 pub fn identity(&self) -> &CandidateId {
111 &self.identity
112 }
113
114 pub fn origin(&self) -> &SourceOrigin {
116 &self.origin
117 }
118
119 pub fn label(&self) -> &str {
121 &self.label
122 }
123
124 pub fn bytes(&self) -> &[u8] {
126 &self.bytes
127 }
128
129 pub fn shared_bytes(&self) -> &Arc<[u8]> {
131 &self.bytes
132 }
133
134 fn with_scoped_identity(mut self, scope: usize) -> Self {
135 self.identity = self.identity.scoped(scope);
136 self
137 }
138}
139
140pub trait Source: Send + Sync {
151 fn find(&self, name: &str) -> io::Result<Option<SourceCandidate>>;
162
163 fn find_candidates<'a>(
172 &'a self,
173 name: &'a str,
174 ) -> Box<dyn Iterator<Item = io::Result<SourceCandidate>> + 'a> {
175 Box::new(
176 std::iter::once_with(move || self.find(name)).filter_map(|result| result.transpose()),
177 )
178 }
179
180 fn list_modules(&self) -> io::Result<Vec<String>>;
190}
191
192pub(crate) struct EmbeddedSource;
194
195impl Source for EmbeddedSource {
196 fn find(&self, name: &str) -> io::Result<Option<SourceCandidate>> {
197 Ok(base_modules::embedded_content(name).map(|content| {
198 SourceCandidate::new(
199 name,
200 SourceOrigin::embedded(name),
201 format!("embedded:{name}"),
202 Arc::<[u8]>::from(content),
203 )
204 }))
205 }
206
207 fn list_modules(&self) -> io::Result<Vec<String>> {
208 Ok(base_modules::base_module_names()
209 .iter()
210 .map(|name| (*name).to_string())
211 .collect())
212 }
213}
214
215#[derive(Clone)]
228pub struct SourceConfig {
229 extensions: Vec<String>,
230}
231
232impl Default for SourceConfig {
233 fn default() -> Self {
234 SourceConfig {
235 extensions: DEFAULT_EXTENSIONS.iter().map(|s| s.to_string()).collect(),
236 }
237 }
238}
239
240impl SourceConfig {
241 pub fn with_extensions(mut self, exts: &[&str]) -> Self {
246 self.extensions = exts
247 .iter()
248 .map(|ext| {
249 let ext = ext.to_lowercase();
250 if !ext.is_empty() && !ext.starts_with('.') {
251 format!(".{ext}")
252 } else {
253 ext
254 }
255 })
256 .collect();
257 self
258 }
259}
260
261struct DirSource {
264 root: PathBuf,
265 index: HashMap<String, Vec<(usize, PathBuf)>>,
266}
267
268pub fn dir(root: impl AsRef<Path>) -> io::Result<Box<dyn Source>> {
293 dir_with_config(root, SourceConfig::default())
294}
295
296pub fn dir_with_config(
305 root: impl AsRef<Path>,
306 config: SourceConfig,
307) -> io::Result<Box<dyn Source>> {
308 let root = root.as_ref();
309 let meta = std::fs::metadata(root)?;
310 if !meta.is_dir() {
311 return Err(io::Error::new(
312 io::ErrorKind::InvalidInput,
313 format!("not a directory: {}", root.display()),
314 ));
315 }
316 let index = build_tree_index(root, &config.extensions)?;
317 Ok(Box::new(DirSource {
318 root: root.to_path_buf(),
319 index,
320 }))
321}
322
323pub fn dirs(roots: impl IntoIterator<Item = impl AsRef<Path>>) -> io::Result<Box<dyn Source>> {
331 let mut sources = Vec::new();
332 for root in roots {
333 sources.push(dir(root)?);
334 }
335 Ok(chain(sources))
336}
337
338impl Source for DirSource {
339 fn find(&self, name: &str) -> io::Result<Option<SourceCandidate>> {
340 self.find_candidates(name).next().transpose()
341 }
342
343 fn find_candidates<'a>(
344 &'a self,
345 name: &'a str,
346 ) -> Box<dyn Iterator<Item = io::Result<SourceCandidate>> + 'a> {
347 let entries = self.index.get(name).into_iter().flatten();
348 Box::new(entries.filter_map(move |(document_index, rel_path)| {
349 let full_path = self.root.join(rel_path);
350 let content = match std::fs::read(&full_path) {
351 Ok(content) => content,
352 Err(error) => return Some(Err(error)),
353 };
354 scan::scan_module_names(&content)
358 .iter()
359 .any(|candidate| candidate == name)
360 .then_some(Ok(SourceCandidate::new(
361 document_index.to_string(),
362 SourceOrigin::file(full_path.clone()),
363 full_path.to_string_lossy().into_owned(),
364 Arc::<[u8]>::from(content),
365 )))
366 }))
367 }
368
369 fn list_modules(&self) -> io::Result<Vec<String>> {
370 let mut names: Vec<String> = self.index.keys().cloned().collect();
371 names.sort();
372 Ok(names)
373 }
374}
375
376struct MultiSource {
379 sources: Vec<Box<dyn Source>>,
380}
381
382pub fn chain(sources: Vec<Box<dyn Source>>) -> Box<dyn Source> {
389 Box::new(MultiSource { sources })
390}
391
392impl Source for MultiSource {
393 fn find(&self, name: &str) -> io::Result<Option<SourceCandidate>> {
394 for (index, src) in self.sources.iter().enumerate() {
395 match src.find(name)? {
396 Some(result) => return Ok(Some(result.with_scoped_identity(index))),
397 None => continue,
398 }
399 }
400 Ok(None)
401 }
402
403 fn find_candidates<'a>(
404 &'a self,
405 name: &'a str,
406 ) -> Box<dyn Iterator<Item = io::Result<SourceCandidate>> + 'a> {
407 Box::new(
408 self.sources
409 .iter()
410 .enumerate()
411 .flat_map(move |(index, source)| {
412 source.find_candidates(name).map(move |candidate| {
413 candidate.map(|item| item.with_scoped_identity(index))
414 })
415 }),
416 )
417 }
418
419 fn list_modules(&self) -> io::Result<Vec<String>> {
420 let mut seen = HashSet::new();
421 let mut names = Vec::new();
422 for src in &self.sources {
423 for name in src.list_modules()? {
424 if seen.insert(name.clone()) {
425 names.push(name);
426 }
427 }
428 }
429 Ok(names)
430 }
431}
432
433pub fn file(path: impl AsRef<Path>) -> io::Result<Box<dyn Source>> {
451 files([path])
452}
453
454pub fn files(paths: impl IntoIterator<Item = impl AsRef<Path>>) -> io::Result<Box<dyn Source>> {
468 let mut modules = HashMap::new();
469 let mut documents = Vec::new();
470 let mut first_path = None;
471 for path in paths {
472 let path = path.as_ref();
473 first_path.get_or_insert_with(|| path.to_path_buf());
474 let content = std::fs::read(path)?;
475 let names = crate::scan::scan_module_names(&content);
476 let document_index = documents.len();
477 let diag_path = path.to_path_buf();
478 documents.push((diag_path.clone(), Arc::<[u8]>::from(content)));
479 for name in names {
480 modules
481 .entry(name)
482 .or_insert_with(Vec::new)
483 .push(document_index);
484 }
485 }
486 if modules.is_empty() {
487 let location = first_path
488 .map(|path| path.display().to_string())
489 .unwrap_or_else(|| "file list".to_string());
490 return Err(io::Error::new(
491 io::ErrorKind::InvalidData,
492 format!("no module definition found in {location}"),
493 ));
494 }
495 Ok(Box::new(FileSource { modules, documents }))
496}
497
498struct FileSource {
500 modules: HashMap<String, Vec<usize>>,
501 documents: Vec<(PathBuf, Arc<[u8]>)>,
502}
503
504impl Source for FileSource {
505 fn find(&self, name: &str) -> io::Result<Option<SourceCandidate>> {
506 self.find_candidates(name).next().transpose()
507 }
508
509 fn find_candidates<'a>(
510 &'a self,
511 name: &'a str,
512 ) -> Box<dyn Iterator<Item = io::Result<SourceCandidate>> + 'a> {
513 Box::new(
514 self.modules
515 .get(name)
516 .into_iter()
517 .flatten()
518 .map(|&document_index| {
519 let (path, bytes) = &self.documents[document_index];
520 Ok(SourceCandidate::new(
521 document_index.to_string(),
522 SourceOrigin::file(path.clone()),
523 path.to_string_lossy().into_owned(),
524 Arc::clone(bytes),
525 ))
526 }),
527 )
528 }
529
530 fn list_modules(&self) -> io::Result<Vec<String>> {
531 let mut names: Vec<String> = self.modules.keys().cloned().collect();
532 names.sort();
533 Ok(names)
534 }
535}
536
537struct MemorySource {
539 modules: HashMap<String, Arc<[u8]>>,
540}
541
542pub fn memory(name: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Box<dyn Source> {
556 memory_modules([(name.into(), bytes.into())])
557}
558
559pub fn memory_modules(
564 modules: impl IntoIterator<Item = (impl Into<String>, impl Into<Vec<u8>>)>,
565) -> Box<dyn Source> {
566 let mut map = HashMap::new();
567 for (name, bytes) in modules {
568 let name = name.into();
569 map.insert(name, Arc::from(bytes.into()));
570 }
571 Box::new(MemorySource { modules: map })
572}
573
574impl Source for MemorySource {
575 fn find(&self, name: &str) -> io::Result<Option<SourceCandidate>> {
576 Ok(self.modules.get(name).map(|bytes| {
577 SourceCandidate::new(
578 name,
579 SourceOrigin::memory(name),
580 format!("<memory:{name}>"),
581 Arc::clone(bytes),
582 )
583 }))
584 }
585
586 fn list_modules(&self) -> io::Result<Vec<String>> {
587 let mut names: Vec<String> = self.modules.keys().cloned().collect();
588 names.sort();
589 Ok(names)
590 }
591}
592
593fn build_tree_index(
595 root: &Path,
596 extensions: &[String],
597) -> io::Result<HashMap<String, Vec<(usize, PathBuf)>>> {
598 let ext_set: HashSet<&str> = extensions.iter().map(|s| s.as_str()).collect();
599 let mut index: HashMap<String, Vec<(usize, PathBuf)>> = HashMap::new();
600 let mut document_index = 0;
601
602 for entry in walkdir::WalkDir::new(root).into_iter() {
603 let entry = match entry {
604 Ok(e) => e,
605 Err(e) => {
606 debug!(
607 target: "mib_rs::source",
608 component = "source",
609 reason = "walkdir_error",
610 error = %e,
611 "skipping directory entry",
612 );
613 continue;
614 }
615 };
616
617 if entry.file_type().is_dir() {
618 continue;
619 }
620
621 let path = entry.path();
622 if !has_valid_extension(path, &ext_set) {
623 continue;
624 }
625
626 let content = match std::fs::read(path) {
627 Ok(c) => c,
628 Err(e) => {
629 debug!(
630 target: "mib_rs::source",
631 component = "source",
632 path = %path.display(),
633 reason = "read_error",
634 error = %e,
635 "cannot read file",
636 );
637 continue;
638 }
639 };
640
641 let names = crate::scan::scan_module_names(&content);
642 let rel_path = path.strip_prefix(root).unwrap_or(path).to_path_buf();
643
644 for name in names {
645 index
646 .entry(name)
647 .or_default()
648 .push((document_index, rel_path.clone()));
649 }
650 document_index += 1;
651 }
652
653 Ok(index)
654}
655
656fn has_valid_extension(path: &Path, ext_set: &HashSet<&str>) -> bool {
657 let ext = path
658 .extension()
659 .map(|e| format!(".{}", e.to_string_lossy().to_lowercase()))
660 .unwrap_or_default();
661 ext_set.contains(ext.as_str())
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 #[test]
669 fn extension_check() {
670 let ext_set: HashSet<&str> = vec!["", ".mib", ".smi"].into_iter().collect();
671 assert!(has_valid_extension(Path::new("IF-MIB"), &ext_set));
672 assert!(has_valid_extension(Path::new("test.mib"), &ext_set));
673 assert!(has_valid_extension(Path::new("test.MIB"), &ext_set));
674 assert!(!has_valid_extension(Path::new("test.txt"), &ext_set));
675 }
676
677 #[test]
678 fn candidate_retains_shared_bytes_and_independent_metadata() {
679 let bytes: Arc<[u8]> = Arc::from(&b"contents"[..]);
680 let candidate = SourceCandidate::new(
681 "document-42",
682 SourceOrigin::custom("workspace", "buffer-42"),
683 "ACME-MIB (modified)",
684 Arc::clone(&bytes),
685 );
686
687 assert_eq!(candidate.identity().as_str(), "document-42");
688 assert_eq!(
689 candidate.origin(),
690 &SourceOrigin::custom("workspace", "buffer-42")
691 );
692 assert_eq!(candidate.label(), "ACME-MIB (modified)");
693 assert_eq!(candidate.bytes().as_ptr(), bytes.as_ptr());
694 assert!(Arc::ptr_eq(candidate.shared_bytes(), &bytes));
695 }
696
697 #[test]
698 fn built_in_non_file_sources_use_typed_origins() {
699 let memory = memory("DISPLAY-NAME", b"bytes".as_slice());
700 let memory_candidate = memory.find("DISPLAY-NAME").unwrap().unwrap();
701 assert_eq!(
702 memory_candidate.origin(),
703 &SourceOrigin::memory("DISPLAY-NAME")
704 );
705 assert_eq!(memory_candidate.label(), "<memory:DISPLAY-NAME>");
706
707 let embedded = EmbeddedSource
708 .find("SNMPv2-SMI")
709 .unwrap()
710 .expect("embedded module");
711 assert_eq!(embedded.origin(), &SourceOrigin::embedded("SNMPv2-SMI"));
712 assert_eq!(embedded.label(), "embedded:SNMPv2-SMI");
713 }
714}