1#![allow(clippy::exhaustive_enums)]
2
3use std::borrow::Cow;
66use std::path::Path;
67use std::path::PathBuf;
68use std::sync::Arc;
69
70use foldhash::HashMap;
71use foldhash::HashMapExt;
72use rayon::iter::IntoParallelIterator;
73use rayon::iter::ParallelIterator;
74
75use crate::change::Change;
76use crate::change::ChangeLog;
77use crate::error::DatabaseError;
78use crate::exclusion::Exclusion;
79use crate::file::File;
80use crate::file::FileId;
81use crate::file::FileType;
82use crate::file::line_starts;
83use crate::operation::FilesystemOperation;
84
85mod utils;
86
87pub mod change;
88pub mod error;
89pub mod exclusion;
90pub mod file;
91pub mod loader;
92pub mod matcher;
93pub mod membership;
94pub mod watcher;
95
96mod operation;
97
98#[derive(Debug, Clone)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct DatabaseConfiguration<'config> {
102 pub workspace: Cow<'config, Path>,
103 pub paths: Vec<Cow<'config, [u8]>>,
106 pub includes: Vec<Cow<'config, [u8]>>,
109 pub patches: Vec<Cow<'config, [u8]>>,
110 pub excludes: Vec<Exclusion<'config>>,
111 pub extensions: Vec<Cow<'config, [u8]>>,
112 pub glob: GlobSettings,
114}
115
116#[derive(Debug, Clone, Copy)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121pub struct GlobSettings {
122 pub case_insensitive: bool,
126 pub literal_separator: bool,
132 pub backslash_escape: bool,
136 pub empty_alternates: bool,
140}
141
142impl Default for GlobSettings {
143 #[inline]
144 fn default() -> Self {
145 Self {
146 case_insensitive: false,
147 literal_separator: false,
148 backslash_escape: !std::path::is_separator('\\'),
149 empty_alternates: false,
150 }
151 }
152}
153
154impl<'config> DatabaseConfiguration<'config> {
155 #[inline]
156 #[must_use]
157 pub fn new(
158 workspace: &'config Path,
159 paths: Vec<&'config [u8]>,
160 includes: Vec<&'config [u8]>,
161 excludes: Vec<Exclusion<'config>>,
162 extensions: Vec<&'config [u8]>,
163 ) -> Self {
164 let paths = paths.into_iter().map(Cow::Borrowed).collect();
165 let includes = includes.into_iter().map(Cow::Borrowed).collect();
166
167 let excludes = excludes
168 .into_iter()
169 .filter_map(|exclusion| match exclusion {
170 Exclusion::Path(p) => Some(if p.is_absolute() {
171 Exclusion::Path(p)
172 } else {
173 workspace.join(p).canonicalize().ok().map(Cow::Owned).map(Exclusion::Path)?
174 }),
175 Exclusion::Pattern(pat) => Some(Exclusion::Pattern(pat)),
176 })
177 .collect();
178
179 let extensions = extensions.into_iter().map(Cow::Borrowed).collect();
180
181 Self {
182 workspace: Cow::Borrowed(workspace),
183 paths,
184 includes,
185 patches: Vec::new(),
186 excludes,
187 extensions,
188 glob: GlobSettings::default(),
189 }
190 }
191
192 #[inline]
193 #[must_use]
194 pub fn into_static(self) -> DatabaseConfiguration<'static> {
195 DatabaseConfiguration {
196 workspace: Cow::Owned(self.workspace.into_owned()),
197 paths: self.paths.into_iter().map(|s| Cow::Owned(s.into_owned())).collect(),
198 includes: self.includes.into_iter().map(|s| Cow::Owned(s.into_owned())).collect(),
199 patches: self.patches.into_iter().map(|s| Cow::Owned(s.into_owned())).collect(),
200 excludes: self
201 .excludes
202 .into_iter()
203 .map(|e| match e {
204 Exclusion::Path(p) => Exclusion::Path(Cow::Owned(p.into_owned())),
205 Exclusion::Pattern(pat) => Exclusion::Pattern(Cow::Owned(pat.into_owned())),
206 })
207 .collect(),
208 extensions: self.extensions.into_iter().map(|s| Cow::Owned(s.into_owned())).collect(),
209 glob: self.glob,
210 }
211 }
212}
213
214#[derive(Debug, Clone)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217#[allow(clippy::field_scoped_visibility_modifiers)]
218pub struct Database<'config> {
219 files: HashMap<Cow<'static, [u8]>, Arc<File>>,
220 id_to_name: HashMap<FileId, Cow<'static, [u8]>>,
221 pub(crate) configuration: DatabaseConfiguration<'config>,
222}
223
224#[derive(Debug)]
226pub struct ReadDatabase {
227 files: Vec<Arc<File>>,
228 id_to_index: HashMap<FileId, usize>,
229 name_to_index: HashMap<Cow<'static, [u8]>, usize>,
230 path_to_index: HashMap<PathBuf, usize>,
231}
232
233impl<'config> Database<'config> {
234 #[inline]
235 #[must_use]
236 pub fn new(configuration: DatabaseConfiguration<'config>) -> Self {
237 Self { files: HashMap::default(), id_to_name: HashMap::default(), configuration }
238 }
239
240 #[inline]
241 #[must_use]
242 pub fn single(file: File, configuration: DatabaseConfiguration<'config>) -> Self {
243 let mut db = Self::new(configuration);
244 db.add(file);
245 db
246 }
247
248 #[inline]
250 pub fn reserve(&mut self, additional: usize) {
251 self.files.reserve(additional);
252 self.id_to_name.reserve(additional);
253 }
254
255 #[inline]
260 pub fn merge_base(&mut self, base: Database<'_>) {
261 let Database { files, mut id_to_name, configuration: _ } = base;
262 self.reserve(files.len());
263
264 for (name, file) in files {
265 if self.files.contains_key(name.as_ref()) {
266 continue;
267 }
268
269 let id = file.id;
270 let id_name = id_to_name.remove(&id).unwrap_or_else(|| name.clone());
271 self.files.insert(name, file);
272 self.id_to_name.insert(id, id_name);
273 }
274 }
275
276 #[inline]
277 pub fn add(&mut self, file: File) -> FileId {
278 let name = file.name.clone();
279 let id = file.id;
280
281 if let Some(old_file) = self.files.insert(name.clone(), Arc::new(file)) {
282 self.id_to_name.remove(&old_file.id);
283 }
284
285 self.id_to_name.insert(id, name);
286
287 id
288 }
289
290 #[inline]
298 pub fn update(&mut self, id: FileId, new_contents: Cow<'static, [u8]>) -> bool {
299 let Some(name) = self.id_to_name.get(&id) else {
300 return false;
301 };
302
303 let Some(arc) = self.files.get_mut(name) else {
304 return false;
305 };
306
307 if let Some(file) = Arc::get_mut(arc) {
308 file.contents = new_contents;
309 file.size = file.contents.len() as u32;
310 file.lines = line_starts(file.contents.as_ref());
311 } else {
312 let old = &**arc;
315 *arc = Arc::new(File::new(old.name.clone(), old.file_type, old.path.clone(), new_contents));
316 }
317
318 true
319 }
320
321 #[inline]
325 pub fn delete(&mut self, id: FileId) -> bool {
326 if let Some(name) = self.id_to_name.remove(&id) { self.files.remove(&name).is_some() } else { false }
327 }
328
329 #[inline]
343 pub fn commit(&mut self, change_log: ChangeLog, write_to_disk: bool) -> Result<(), DatabaseError> {
344 let changes = change_log.into_inner()?;
345 let mut fs_operations = Vec::new();
346
347 for change in changes {
348 match change {
349 Change::Add(file) => {
350 if write_to_disk && let Some(path) = &file.path {
351 fs_operations.push(FilesystemOperation::Write(path.clone(), file.contents.clone()));
352 }
353
354 self.add(file);
355 }
356 Change::Update(id, contents) => {
357 if write_to_disk
358 && let Ok(file) = self.get(&id)
359 && let Some(path) = &file.path
360 {
361 fs_operations.push(FilesystemOperation::Write(path.clone(), contents.clone()));
362 }
363
364 self.update(id, contents);
365 }
366 Change::Delete(id) => {
367 if write_to_disk
368 && let Ok(file) = self.get(&id)
369 && let Some(path) = &file.path
370 {
371 fs_operations.push(FilesystemOperation::Delete(path.clone()));
372 }
373
374 self.delete(id);
375 }
376 }
377 }
378
379 if write_to_disk {
380 fs_operations.into_par_iter().try_for_each(|op| -> Result<(), DatabaseError> { op.execute() })?;
381 }
382
383 Ok(())
384 }
385
386 #[inline]
393 #[must_use]
394 pub fn read_only(&self) -> ReadDatabase {
395 let mut files_vec: Vec<Arc<File>> = self.files.values().cloned().collect();
396 files_vec.sort_unstable_by_key(|f| f.id);
397
398 let mut id_to_index = HashMap::with_capacity(files_vec.len());
399 let mut name_to_index = HashMap::with_capacity(files_vec.len());
400 let mut path_to_index = HashMap::with_capacity(files_vec.len());
401
402 for (index, file) in files_vec.iter().enumerate() {
403 id_to_index.insert(file.id, index);
404 name_to_index.insert(file.name.clone(), index);
405 if let Some(path) = &file.path {
406 path_to_index.insert(path.clone(), index);
407 }
408 }
409
410 ReadDatabase { files: files_vec, id_to_index, name_to_index, path_to_index }
411 }
412}
413
414impl ReadDatabase {
415 #[inline]
416 #[must_use]
417 pub fn empty() -> Self {
418 Self {
419 files: Vec::new(),
420 id_to_index: HashMap::new(),
421 name_to_index: HashMap::new(),
422 path_to_index: HashMap::new(),
423 }
424 }
425
426 #[inline]
436 #[must_use]
437 pub fn single(file: File) -> Self {
438 let mut id_to_index = HashMap::with_capacity(1);
439 let mut name_to_index = HashMap::with_capacity(1);
440 let mut path_to_index = HashMap::with_capacity(1);
441
442 id_to_index.insert(file.id, 0);
443 name_to_index.insert(file.name.clone(), 0);
444 if let Some(path) = &file.path {
445 path_to_index.insert(path.clone(), 0);
446 }
447
448 Self { files: vec![Arc::new(file)], id_to_index, name_to_index, path_to_index }
449 }
450}
451
452pub trait DatabaseReader {
458 fn get_id(&self, name: &[u8]) -> Option<FileId>;
460
461 fn get(&self, id: &FileId) -> Result<Arc<File>, DatabaseError>;
467
468 fn get_ref(&self, id: &FileId) -> Result<&File, DatabaseError>;
474
475 fn get_by_name(&self, name: &[u8]) -> Result<Arc<File>, DatabaseError>;
481
482 fn get_by_path(&self, path: &Path) -> Result<Arc<File>, DatabaseError>;
488
489 fn files(&self) -> impl Iterator<Item = Arc<File>>;
494
495 #[inline]
497 fn files_with_type(&self, file_type: FileType) -> impl Iterator<Item = Arc<File>> {
498 self.files().filter(move |file| file.file_type == file_type)
499 }
500
501 #[inline]
503 fn files_without_type(&self, file_type: FileType) -> impl Iterator<Item = Arc<File>> {
504 self.files().filter(move |file| file.file_type != file_type)
505 }
506
507 #[inline]
509 fn file_ids(&self) -> impl Iterator<Item = FileId> {
510 self.files().map(|file| file.id)
511 }
512
513 #[inline]
515 fn file_ids_with_type(&self, file_type: FileType) -> impl Iterator<Item = FileId> {
516 self.files_with_type(file_type).map(|file| file.id)
517 }
518
519 #[inline]
521 fn file_ids_without_type(&self, file_type: FileType) -> impl Iterator<Item = FileId> {
522 self.files_without_type(file_type).map(|file| file.id)
523 }
524
525 fn len(&self) -> usize;
527
528 #[inline]
530 fn is_empty(&self) -> bool {
531 self.len() == 0
532 }
533}
534
535impl DatabaseReader for Database<'_> {
536 #[inline]
537 fn get_id(&self, name: &[u8]) -> Option<FileId> {
538 self.files.get(name).map(|f| f.id)
539 }
540
541 #[inline]
542 fn get(&self, id: &FileId) -> Result<Arc<File>, DatabaseError> {
543 let name = self.id_to_name.get(id).ok_or(DatabaseError::FileNotFound)?;
544 let file = self.files.get(name).ok_or(DatabaseError::FileNotFound)?;
545
546 Ok(Arc::clone(file))
547 }
548
549 #[inline]
550 fn get_ref(&self, id: &FileId) -> Result<&File, DatabaseError> {
551 let name = self.id_to_name.get(id).ok_or(DatabaseError::FileNotFound)?;
552 self.files.get(name).map(std::convert::AsRef::as_ref).ok_or(DatabaseError::FileNotFound)
553 }
554
555 #[inline]
556 fn get_by_name(&self, name: &[u8]) -> Result<Arc<File>, DatabaseError> {
557 self.files.get(name).cloned().ok_or(DatabaseError::FileNotFound)
558 }
559
560 #[inline]
561 fn get_by_path(&self, path: &Path) -> Result<Arc<File>, DatabaseError> {
562 self.files.values().find(|file| file.path.as_deref() == Some(path)).cloned().ok_or(DatabaseError::FileNotFound)
563 }
564
565 #[inline]
566 fn files(&self) -> impl Iterator<Item = Arc<File>> {
567 self.files.values().cloned()
568 }
569
570 #[inline]
571 fn len(&self) -> usize {
572 self.files.len()
573 }
574}
575
576impl DatabaseReader for ReadDatabase {
577 #[inline]
578 fn get_id(&self, name: &[u8]) -> Option<FileId> {
579 self.name_to_index.get(name).and_then(|&i| self.files.get(i)).map(|f| f.id)
580 }
581
582 #[inline]
583 fn get(&self, id: &FileId) -> Result<Arc<File>, DatabaseError> {
584 let index = self.id_to_index.get(id).ok_or(DatabaseError::FileNotFound)?;
585
586 self.files.get(*index).cloned().ok_or(DatabaseError::FileNotFound)
587 }
588
589 #[inline]
590 fn get_ref(&self, id: &FileId) -> Result<&File, DatabaseError> {
591 let index = self.id_to_index.get(id).ok_or(DatabaseError::FileNotFound)?;
592
593 self.files.get(*index).map(std::convert::AsRef::as_ref).ok_or(DatabaseError::FileNotFound)
594 }
595
596 #[inline]
597 fn get_by_name(&self, name: &[u8]) -> Result<Arc<File>, DatabaseError> {
598 self.name_to_index.get(name).and_then(|&i| self.files.get(i)).cloned().ok_or(DatabaseError::FileNotFound)
599 }
600
601 #[inline]
602 fn get_by_path(&self, path: &Path) -> Result<Arc<File>, DatabaseError> {
603 self.path_to_index.get(path).and_then(|&i| self.files.get(i)).cloned().ok_or(DatabaseError::FileNotFound)
604 }
605
606 #[inline]
607 fn files(&self) -> impl Iterator<Item = Arc<File>> {
608 self.files.iter().cloned()
609 }
610
611 #[inline]
612 fn len(&self) -> usize {
613 self.files.len()
614 }
615}
616
617#[cfg(test)]
618#[allow(clippy::unwrap_used)]
619mod tests {
620 use super::*;
621
622 fn configuration() -> DatabaseConfiguration<'static> {
623 DatabaseConfiguration::new(Path::new("/"), vec![], vec![], vec![], vec![]).into_static()
624 }
625
626 #[test]
627 fn merge_base_adds_missing_files_without_replacing_project_files() {
628 let mut base = Database::new(configuration());
629 base.add(File::new(Cow::Borrowed(b"shared.php"), FileType::Builtin, None, Cow::Borrowed(b"base")));
630 base.add(File::new(Cow::Borrowed(b"builtin.php"), FileType::Builtin, None, Cow::Borrowed(b"builtin")));
631
632 let mut project = Database::new(configuration());
633 project.add(File::new(Cow::Borrowed(b"shared.php"), FileType::Host, None, Cow::Borrowed(b"project")));
634 project.add(File::new(Cow::Borrowed(b"project.php"), FileType::Host, None, Cow::Borrowed(b"project-only")));
635
636 project.merge_base(base);
637
638 assert_eq!(project.len(), 3);
639 let shared = project.get_by_name(b"shared.php").unwrap();
640 assert_eq!(shared.file_type, FileType::Host);
641 assert_eq!(shared.contents.as_ref(), b"project");
642
643 let builtin = project.get_by_name(b"builtin.php").unwrap();
644 assert_eq!(builtin.file_type, FileType::Builtin);
645 assert_eq!(project.get(&builtin.id).unwrap().name.as_ref(), b"builtin.php");
646 }
647}