1use std::io::Read;
2use std::path::{Path, PathBuf};
3#[cfg(not(target_os = "windows"))]
4use std::sync::OnceLock;
5use std::sync::atomic::{AtomicI32, AtomicU8, AtomicU64, AtomicUsize, Ordering};
6
7#[cfg(not(target_os = "windows"))]
8use crate::constants::{FRESH_MMAP_THRESHOLD, MMAP_THRESHOLD};
9use crate::constants::{MAX_CACHED_CONTENT_BYTES, MAX_FFFILE_SIZE, PATH_BUF_SIZE};
10use crate::constraints::Constrainable;
11use crate::query_tracker::QueryMatchEntry;
12use crate::simd_path::ArenaPtr;
13use fff_query_parser::{FFFQuery, FuzzyQuery, Location};
14
15pub trait FFFStringStorage {
19 fn arena_for(&self, file: &FileItem) -> ArenaPtr;
21
22 fn base_arena(&self) -> ArenaPtr;
24 fn overflow_arena(&self) -> ArenaPtr;
26}
27
28impl FFFStringStorage for ArenaPtr {
29 #[inline]
30 fn arena_for(&self, _file: &FileItem) -> ArenaPtr {
31 *self
32 }
33
34 #[inline]
35 fn base_arena(&self) -> ArenaPtr {
36 *self
37 }
38
39 #[inline]
40 fn overflow_arena(&self) -> ArenaPtr {
41 *self
42 }
43}
44
45pub trait FileSliceExt {
46 fn live_count(&self) -> usize;
47}
48
49impl FileSliceExt for [FileItem] {
50 #[inline]
51 fn live_count(&self) -> usize {
52 self.iter().filter(|f| !f.is_deleted()).count()
53 }
54}
55
56pub struct FileItemFlags;
57
58impl FileItemFlags {
59 pub const BINARY: u8 = 1 << 0;
60 pub const DELETED: u8 = 1 << 1;
63 pub const OVERFLOW: u8 = 1 << 2;
66}
67
68pub struct DirFlags;
69
70impl DirFlags {
71 pub const OVERFLOW: u8 = 1 << 0;
72}
73
74#[derive(Debug)]
76pub struct DirItem {
77 flags: u8,
78 pub(crate) path: crate::simd_path::ChunkedString,
79 last_segment_offset: u16,
82 max_access_frecency: AtomicI32,
85}
86
87impl Clone for DirItem {
88 fn clone(&self) -> Self {
89 Self {
90 flags: self.flags,
91 path: self.path.clone(),
92 last_segment_offset: self.last_segment_offset,
93 max_access_frecency: AtomicI32::new(self.max_access_frecency()),
94 }
95 }
96}
97
98impl DirItem {
99 #[inline(always)]
100 pub fn is_overflow(&self) -> bool {
101 self.flags & DirFlags::OVERFLOW != 0
102 }
103
104 pub(crate) fn new(path: crate::simd_path::ChunkedString, last_segment_offset: u16) -> Self {
105 Self {
106 path,
107 flags: 0,
108 last_segment_offset,
109 max_access_frecency: AtomicI32::new(0),
110 }
111 }
112
113 #[inline]
115 pub fn last_segment_offset(&self) -> u16 {
116 self.last_segment_offset
117 }
118
119 #[inline]
121 pub fn max_access_frecency(&self) -> i32 {
122 self.max_access_frecency.load(Ordering::Relaxed)
123 }
124
125 #[inline]
128 pub fn update_frecency_if_larger(&self, score: i32) {
129 self.max_access_frecency.fetch_max(score, Ordering::Relaxed);
130 }
131
132 #[inline]
134 pub fn reset_frecency(&self) {
135 self.max_access_frecency.store(0, Ordering::Relaxed);
136 }
137
138 pub(crate) fn read_relative_path<'a>(&self, arena: ArenaPtr, buf: &'a mut [u8]) -> &'a str {
139 self.path.read_to_buf(arena, buf)
140 }
141
142 pub fn relative_path(&self, arena: impl FFFStringStorage) -> String {
144 let mut out = String::new();
145 let ptr = if self.is_overflow() {
146 arena.overflow_arena()
147 } else {
148 arena.base_arena()
149 };
150
151 self.path.write_to_string(ptr, &mut out);
152 out
153 }
154
155 pub fn write_dir_name(&self, arena: ArenaPtr, out: &mut String) {
157 out.clear();
158 let total = self.path.byte_len as usize;
159 let offset = self.last_segment_offset as usize;
160 if offset >= total {
161 return;
162 }
163 let mut buf = [0u8; PATH_BUF_SIZE];
165 let full = self.path.read_to_buf(arena, &mut buf);
166 out.push_str(&full[offset..]);
167 }
168
169 pub fn dir_name(&self, arena: impl FFFStringStorage) -> String {
171 let mut out = String::new();
172 let ptr = if self.is_overflow() {
173 arena.overflow_arena()
174 } else {
175 arena.base_arena()
176 };
177 self.write_dir_name(ptr, &mut out);
178 out
179 }
180
181 pub fn absolute_path(&self, arena: impl FFFStringStorage, base_path: &Path) -> PathBuf {
183 let rel = self.relative_path(arena);
184 if rel.is_empty() {
185 base_path.to_path_buf()
186 } else {
187 base_path.join(&rel)
188 }
189 }
190}
191
192impl Constrainable for DirItem {
193 #[inline]
194 fn write_file_name(&self, arena: ArenaPtr, out: &mut String) {
195 self.write_dir_name(arena, out);
197 }
198
199 #[inline]
200 fn write_relative_path(&self, arena: ArenaPtr, out: &mut String) {
201 self.path.write_to_string(arena, out);
202 }
203
204 #[inline]
205 fn git_status(&self) -> Option<git2::Status> {
206 None
207 }
208
209 #[inline]
210 fn is_overflow(&self) -> bool {
211 DirItem::is_overflow(self)
212 }
213}
214
215#[derive(Debug)]
216pub struct FileItem {
217 pub size: u64,
218 pub modified: u64,
219 pub access_frecency_score: i16,
220 pub modification_frecency_score: i16,
221 pub git_status: Option<git2::Status>,
222 pub(crate) path: crate::simd_path::ChunkedString,
223 pub(crate) parent_dir_index: u32,
224 flags: AtomicU8,
225 #[cfg(not(target_os = "windows"))]
227 content: OnceLock<memmap2::Mmap>,
228}
229
230impl Clone for FileItem {
231 fn clone(&self) -> Self {
232 Self {
233 path: self.path.clone(),
234 parent_dir_index: self.parent_dir_index,
235 size: self.size,
236 modified: self.modified,
237 access_frecency_score: self.access_frecency_score,
238 modification_frecency_score: self.modification_frecency_score,
239 git_status: self.git_status,
240 flags: AtomicU8::new(self.flags.load(Ordering::Relaxed)),
241 #[cfg(not(target_os = "windows"))]
243 content: OnceLock::new(),
244 }
245 }
246}
247
248pub const BINARY_CLASSIFICATION_CHUNK_SIZE: usize = 16 * 1024;
253
254#[inline]
256pub(crate) fn detect_binary_content(content: &[u8]) -> bool {
257 memchr::memchr(0, content).is_some()
258}
259
260impl FileItem {
261 pub fn new_raw(
262 filename_start: u16,
263 size: u64,
264 modified: u64,
265 git_status: Option<git2::Status>,
266 is_binary: bool,
267 ) -> Self {
268 let mut flags = 0u8;
269 if is_binary {
270 flags |= FileItemFlags::BINARY;
271 }
272
273 let mut path = crate::simd_path::ChunkedString::empty();
274 path.filename_offset = filename_start;
275
276 Self {
277 path,
278 parent_dir_index: u32::MAX,
279 size,
280 modified,
281 access_frecency_score: 0,
282 modification_frecency_score: 0,
283 git_status,
284 flags: AtomicU8::new(flags),
285 #[cfg(not(target_os = "windows"))]
286 content: OnceLock::new(),
287 }
288 }
289
290 pub fn absolute_path(&self, arena: impl FFFStringStorage, base_path: &Path) -> PathBuf {
292 let mut buf = [0u8; PATH_BUF_SIZE];
293 let rel = self.path.read_to_buf(arena.arena_for(self), &mut buf);
294 base_path.join(rel)
295 }
296
297 pub(crate) fn set_path(&mut self, path: crate::simd_path::ChunkedString) {
298 self.path = path;
299 }
300
301 pub fn dir_str(&self, arena: impl FFFStringStorage) -> String {
302 let mut s = String::with_capacity(64);
303 self.path.write_dir_to(arena.arena_for(self), &mut s);
304 s
305 }
306
307 pub(crate) fn write_dir_str(&self, arena: ArenaPtr, out: &mut String) {
308 self.path.write_dir_to(arena, out);
309 }
310
311 pub fn file_name(&self, arena: impl FFFStringStorage) -> String {
312 let mut s = String::with_capacity(32);
313 self.path.write_filename_to(arena.arena_for(self), &mut s);
314 s
315 }
316
317 pub(crate) fn write_file_name_from_arena(&self, arena: ArenaPtr, out: &mut String) {
318 self.path.write_filename_to(arena, out);
319 }
320
321 pub fn relative_path(&self, arena: impl FFFStringStorage) -> String {
322 let mut s = String::with_capacity(64);
323 self.path.write_to_string(arena.arena_for(self), &mut s);
324 s
325 }
326
327 pub(crate) fn write_relative_path_from_arena(&self, arena: ArenaPtr, out: &mut String) {
328 self.path.write_to_string(arena, out);
329 }
330
331 pub fn relative_path_len(&self) -> usize {
332 self.path.byte_len as usize
333 }
334
335 pub fn filename_offset_in_relative_path(&self) -> usize {
336 self.path.filename_offset as usize
337 }
338
339 pub(crate) fn relative_path_eq(&self, arena: ArenaPtr, other: &str) -> bool {
340 if other.len() != self.path.byte_len as usize {
341 return false;
342 }
343 let mut buf = [0u8; 512];
344 let mine = self.path.read_to_buf(arena, &mut buf);
345 mine == other
346 }
347
348 pub(crate) fn relative_path_starts_with(&self, arena: ArenaPtr, prefix: &str) -> bool {
349 let mut buf = [0u8; PATH_BUF_SIZE];
350 let path = self.path.read_to_buf(arena, &mut buf);
351 path.starts_with(prefix)
352 }
353
354 pub(crate) fn write_absolute_path<'a>(
358 &self,
359 arena: ArenaPtr,
360 base_path: &Path,
361 buf: &'a mut [u8; PATH_BUF_SIZE],
362 ) -> &'a Path {
363 let base = base_path.as_os_str().as_encoded_bytes();
364 let base_len = base.len();
365 buf[..base_len].copy_from_slice(base);
366 let sep_len = if base_len > 0 && base[base_len - 1] != std::path::MAIN_SEPARATOR as u8 {
367 buf[base_len] = std::path::MAIN_SEPARATOR as u8;
368 1
369 } else {
370 0
371 };
372
373 let base_end_idx = base_len + sep_len;
374 let relative_portion_str = self.path.read_to_buf(arena, &mut buf[base_end_idx..]);
375 let total = base_end_idx + relative_portion_str.len();
376 Path::new(unsafe { std::str::from_utf8_unchecked(&buf[..total]) })
377 }
378
379 #[cfg(unix)]
386 pub(crate) fn write_relative_cstr<'a>(
387 &self,
388 arena: ArenaPtr,
389 buf: &'a mut [u8; PATH_BUF_SIZE],
390 ) -> &'a std::ffi::CStr {
391 let rel = self.path.read_to_buf(arena, &mut buf[..PATH_BUF_SIZE - 1]);
393 let n = rel.len();
394 buf[n] = 0;
395 unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(&buf[..=n]) }
398 }
399
400 #[inline]
401 pub fn total_frecency_score(&self) -> i32 {
402 self.access_frecency_score as i32 + self.modification_frecency_score as i32
403 }
404
405 #[allow(dead_code)]
406 #[inline]
407 pub(crate) fn is_likely_hot(&self) -> bool {
408 self.access_frecency_score > 0 || self.git_status.is_some()
409 }
410
411 #[inline]
413 pub(crate) fn read_trimmed_into_buf(
414 &self,
415 base_fd: i32,
416 base_path: &Path,
417 arena: ArenaPtr,
418 path_buf: &mut [u8; PATH_BUF_SIZE],
419 buf: &mut [u8],
420 ) -> usize {
421 #[cfg(unix)]
422 {
423 self.read_into_buf_unix(base_fd, base_path, arena, path_buf, buf)
424 }
425 #[cfg(not(unix))]
426 {
427 let _ = base_fd;
428 self.read_into_buf_std(base_path, arena, path_buf, buf)
429 }
430 }
431
432 #[cfg(unix)]
433 fn read_into_buf_unix(
434 &self,
435 base_fd: libc::c_int,
436 base_path: &Path,
437 arena: ArenaPtr,
438 path_buf: &mut [u8; PATH_BUF_SIZE],
439 buf: &mut [u8],
440 ) -> usize {
441 let fd = if base_fd >= 0 {
442 let relative_path = self.write_relative_cstr(arena, path_buf);
443 unsafe { libc::openat(base_fd, relative_path.as_ptr(), libc::O_RDONLY) }
446 } else {
447 use std::os::unix::io::IntoRawFd;
448 let abs = self.write_absolute_path(arena, base_path, path_buf);
449 match std::fs::File::open(abs) {
450 Ok(f) => f.into_raw_fd(),
451 Err(e) => {
452 tracing::error!(?e, "Failed to fopen file");
453 return 0;
454 }
455 }
456 };
457 if fd < 0 {
458 return 0;
459 }
460
461 let mut filled = 0usize;
462 while filled < buf.len() {
463 let n = unsafe {
466 libc::read(
467 fd,
468 buf[filled..].as_mut_ptr() as *mut libc::c_void,
469 (buf.len() - filled) as libc::size_t,
470 )
471 };
472 if n <= 0 {
473 break;
474 }
475 filled += n as usize;
476 }
477
478 unsafe { libc::close(fd) };
480 filled
481 }
482
483 #[cfg(not(unix))]
484 fn read_into_buf_std(
485 &self,
486 base_path: &Path,
487 arena: ArenaPtr,
488 path_buf: &mut [u8; PATH_BUF_SIZE],
489 buf: &mut [u8],
490 ) -> usize {
491 let abs = self.write_absolute_path(arena, base_path, path_buf);
492 let Ok(mut f) = std::fs::File::open(abs) else {
493 return 0;
494 };
495 let mut filled = 0usize;
496 while filled < buf.len() {
497 match f.read(&mut buf[filled..]) {
498 Ok(0) => break,
499 Ok(n) => filled += n,
500 Err(_) => return 0,
501 }
502 }
503 filled
504 }
505
506 #[inline]
507 pub fn is_binary(&self) -> bool {
508 self.flags.load(Ordering::Relaxed) & FileItemFlags::BINARY != 0
509 }
510
511 #[inline]
512 pub fn set_binary(&self, val: bool) {
513 if val {
514 self.flags
515 .fetch_or(FileItemFlags::BINARY, Ordering::Relaxed);
516 } else {
517 self.flags
518 .fetch_and(!FileItemFlags::BINARY, Ordering::Relaxed);
519 }
520 }
521
522 pub(crate) fn detect_binary_per_byte(&self, path: &Path, chunk: &mut [u8]) {
525 if self.size == 0 {
526 return;
527 }
528
529 let Ok(mut file) = std::fs::OpenOptions::new()
530 .write(false)
531 .read(true)
532 .open(path)
533 else {
534 tracing::error!(path = ?path.display(), "Failed to open indexed file");
535 return;
536 };
537
538 loop {
539 match file.read(chunk) {
540 Ok(0) => break,
541 Err(e) => {
542 tracing::error!(?e, "Failed to read file chunk");
543 break;
544 }
545 Ok(n) => {
546 if detect_binary_content(&chunk[..n]) {
547 self.set_binary(true);
548 }
549 }
550 }
551 }
552 }
553
554 #[inline]
555 pub fn is_deleted(&self) -> bool {
556 self.flags.load(Ordering::Relaxed) & FileItemFlags::DELETED != 0
557 }
558
559 #[inline]
560 #[doc(hidden)]
561 pub fn set_deleted(&self, val: bool) {
563 if val {
564 self.flags
565 .fetch_or(FileItemFlags::DELETED, Ordering::Relaxed);
566 } else {
567 self.flags
568 .fetch_and(!FileItemFlags::DELETED, Ordering::Relaxed);
569 }
570 }
571
572 #[inline]
573 pub fn is_overflow(&self) -> bool {
574 self.flags.load(Ordering::Relaxed) & FileItemFlags::OVERFLOW != 0
575 }
576
577 #[inline]
578 pub fn set_overflow(&self, val: bool) {
579 if val {
580 self.flags
581 .fetch_or(FileItemFlags::OVERFLOW, Ordering::Relaxed);
582 } else {
583 self.flags
584 .fetch_and(!FileItemFlags::OVERFLOW, Ordering::Relaxed);
585 }
586 }
587}
588
589impl FileItem {
590 #[cfg(not(target_os = "windows"))]
597 pub fn invalidate_mmap(&mut self, budget: &ContentCacheBudget) {
598 if self.content.get().is_some() {
599 budget.cached_count.fetch_sub(1, Ordering::Relaxed);
600 budget.cached_bytes.fetch_sub(self.size, Ordering::Relaxed);
601 }
602
603 self.content = OnceLock::new();
604 }
605
606 #[cfg(target_os = "windows")]
607 pub fn invalidate_mmap(&mut self, _: &ContentCacheBudget) {}
608
609 pub fn update_metadata(
610 &mut self,
611 budget: &ContentCacheBudget,
612 modified_secs: Option<u64>,
613 new_size: Option<u64>,
614 ) {
615 if let Some(modified) = modified_secs
616 && self.modified < modified
617 {
618 self.modified = modified;
619 }
620
621 self.invalidate_mmap(budget);
622
623 if let Some(size) = new_size {
624 self.size = size;
625 }
626 }
627
628 #[cfg(target_os = "windows")]
641 pub(crate) fn get_cached_content(
642 &self,
643 _arena: ArenaPtr,
644 _base_path: &Path,
645 _budget: &ContentCacheBudget,
646 ) -> Option<&[u8]> {
647 None
648 }
649
650 #[cfg(not(target_os = "windows"))]
654 pub(crate) fn get_cached_content(
655 &self,
656 arena: ArenaPtr,
657 base_path: &Path,
658 budget: &ContentCacheBudget,
659 ) -> Option<&[u8]> {
660 if let Some(content) = self.content.get() {
661 return Some(content);
662 }
663
664 if self.size < MMAP_THRESHOLD || self.size > budget.max_file_size {
665 return None;
666 }
667
668 let count = budget.cached_count.load(Ordering::Relaxed);
670 let bytes = budget.cached_bytes.load(Ordering::Relaxed);
671 let max_files = budget.max_files;
672 let max_bytes = budget.max_bytes;
673 if count >= max_files || bytes + self.size > max_bytes {
674 return None;
675 }
676
677 let path = self.absolute_path(arena, base_path);
678 let file = std::fs::File::open(&path).ok()?;
679 let mmap = unsafe { memmap2::Mmap::map(&file) }.ok()?;
683 let result = self.content.get_or_init(|| mmap);
684
685 budget.cached_count.fetch_add(1, Ordering::Relaxed);
686 budget.cached_bytes.fetch_add(self.size, Ordering::Relaxed);
687
688 Some(result)
689 }
690
691 #[inline]
697 pub(crate) fn get_content_for_search<'a>(
698 &'a self,
699 buf: &'a mut Vec<u8>,
700 #[cfg_attr(target_os = "windows", allow(unused_variables))] mmap_slot: &'a mut MmapSlot,
701 arena: ArenaPtr,
702 base_path: &Path,
703 budget: &ContentCacheBudget,
704 ) -> Option<&'a [u8]> {
705 #[cfg(not(target_os = "windows"))]
706 {
707 if let Some(cached) = self.get_cached_content(arena, base_path, budget) {
711 return Some(cached);
712 }
713 }
714
715 let max_file_size = budget.max_file_size;
716 if self.is_binary() || self.size == 0 || self.size > max_file_size {
717 return None;
718 }
719
720 let abs = self.absolute_path(arena, base_path);
721
722 #[cfg(not(target_os = "windows"))]
723 if self.size >= FRESH_MMAP_THRESHOLD {
724 let file = std::fs::File::open(&abs).ok()?;
725 let mmap = unsafe { memmap2::Mmap::map(&file) }.ok()?;
726 let stored = mmap_slot.insert(mmap);
727 return Some(&stored[..]);
728 } else {
729 let _ = (mmap_slot, arena);
730 }
731
732 let len = self.size as usize;
733 buf.resize(len, 0);
734
735 let mut file = std::fs::File::open(&abs).ok()?;
736 file.read_exact(buf).ok()?;
737 Some(buf.as_slice())
738 }
739}
740
741#[cfg(not(target_os = "windows"))]
745pub type MmapSlot = Option<memmap2::Mmap>;
746#[cfg(target_os = "windows")]
747pub type MmapSlot = ();
748
749impl Constrainable for FileItem {
750 #[inline]
751 fn write_file_name(&self, arena: ArenaPtr, out: &mut String) {
752 self.path.write_filename_to(arena, out);
753 }
754
755 #[inline]
756 fn write_relative_path(&self, arena: ArenaPtr, out: &mut String) {
757 self.path.write_to_string(arena, out);
758 }
759
760 #[inline]
761 fn git_status(&self) -> Option<git2::Status> {
762 self.git_status
763 }
764
765 #[inline]
766 fn is_overflow(&self) -> bool {
767 FileItem::is_overflow(self)
768 }
769}
770
771#[derive(Debug, Clone, Default)]
772pub struct Score {
773 pub total: i32,
774 pub base_score: i32,
775 pub filename_bonus: i32,
776 pub special_filename_bonus: i32,
777 pub frecency_boost: i32,
778 pub git_status_boost: i32,
779 pub distance_penalty: i32,
780 pub current_file_penalty: i32,
781 pub combo_match_boost: i32,
782 pub path_alignment_bonus: i32,
783 pub exact_match: bool,
784 pub match_type: &'static str,
785}
786
787#[derive(Debug, Clone, Copy)]
788pub struct PaginationArgs {
789 pub offset: usize,
790 pub limit: usize,
791}
792
793impl Default for PaginationArgs {
794 fn default() -> Self {
795 Self {
796 offset: 0,
797 limit: 100,
798 }
799 }
800}
801
802#[derive(Debug, Clone)]
803pub struct ScoringContext<'a> {
804 pub query: &'a FFFQuery<'a>,
805 pub project_path: Option<&'a Path>,
806 pub current_file: Option<&'a str>,
807 pub max_typos: u16,
808 pub max_threads: usize,
809 pub last_same_query_match: Option<QueryMatchEntry>,
810 pub combo_boost_score_multiplier: i32,
811 pub min_combo_count: u32,
812 pub pagination: PaginationArgs,
813}
814
815impl ScoringContext<'_> {
816 pub fn effective_query(&self) -> &str {
817 match &self.query.fuzzy_query {
818 FuzzyQuery::Text(t) => t,
819 FuzzyQuery::Parts(parts) if !parts.is_empty() => parts[0],
820 _ => self.query.raw_query.trim(),
821 }
822 }
823}
824
825#[derive(Debug, Clone, Default)]
826pub struct SearchResult<'a> {
827 pub items: Vec<&'a FileItem>,
828 pub scores: Vec<Score>,
829 pub total_matched: usize,
830 pub total_files: usize,
831 pub location: Option<Location>,
832}
833
834#[derive(Debug, Clone, Default)]
836pub struct DirSearchResult<'a> {
837 pub items: Vec<&'a DirItem>,
838 pub scores: Vec<Score>,
839 pub total_matched: usize,
840 pub total_dirs: usize,
841}
842
843#[derive(Debug, Clone)]
845pub enum MixedItemRef<'a> {
846 File(&'a FileItem),
847 Dir(&'a DirItem),
848}
849
850#[derive(Debug, Clone, Default)]
853pub struct MixedSearchResult<'a> {
854 pub items: Vec<MixedItemRef<'a>>,
855 pub scores: Vec<Score>,
856 pub total_matched: usize,
857 pub total_files: usize,
858 pub total_dirs: usize,
859 pub location: Option<Location>,
860}
861
862impl Default for MixedItemRef<'_> {
863 fn default() -> Self {
864 unreachable!("MixedItemRef::default should not be called")
866 }
867}
868
869#[derive(Debug)]
870pub struct ContentCacheBudget {
871 pub max_files: usize,
872 pub max_bytes: u64,
873 pub max_file_size: u64,
874 pub cached_count: AtomicUsize,
875 pub cached_bytes: AtomicU64,
876}
877
878impl ContentCacheBudget {
879 pub fn unlimited() -> Self {
880 Self {
881 max_files: usize::MAX,
882 max_bytes: u64::MAX,
883 max_file_size: MAX_FFFILE_SIZE,
884 cached_count: AtomicUsize::new(0),
885 cached_bytes: AtomicU64::new(0),
886 }
887 }
888
889 pub fn zero() -> Self {
890 Self {
891 max_files: 0,
892 max_bytes: 0,
893 max_file_size: 0,
894 cached_count: AtomicUsize::new(0),
895 cached_bytes: AtomicU64::new(0),
896 }
897 }
898
899 pub fn is_exhausted(&self) -> bool {
901 self.cached_count.load(Ordering::Relaxed) >= self.max_files
902 || self.cached_bytes.load(Ordering::Relaxed) >= self.max_bytes
903 }
904
905 pub fn new_for_repo(file_count: usize) -> Self {
906 let max_files = if file_count > 50_000 {
907 5_000
908 } else if file_count > 10_000 {
909 10_000
910 } else {
911 30_000 };
913
914 let max_bytes = if file_count > 50_000 {
915 128 * 1024 * 1024 } else if file_count > 10_000 {
917 256 * 1024 * 1024 } else {
919 MAX_CACHED_CONTENT_BYTES };
921
922 Self {
923 max_files,
924 max_bytes,
925 max_file_size: MAX_FFFILE_SIZE,
926 cached_count: AtomicUsize::new(0),
927 cached_bytes: AtomicU64::new(0),
928 }
929 }
930
931 pub fn from_overrides(max_files: usize, max_bytes: u64, max_file_size: u64) -> Option<Self> {
939 if max_files == 0 && max_bytes == 0 && max_file_size == 0 {
940 return None;
941 }
942
943 let mut budget = Self::default();
944 if max_files > 0 {
945 budget.max_files = max_files;
946 }
947 if max_bytes > 0 {
948 budget.max_bytes = max_bytes;
949 }
950 if max_file_size > 0 {
951 budget.max_file_size = max_file_size;
952 }
953 Some(budget)
954 }
955
956 pub fn reset(&self) {
957 self.cached_count.store(0, Ordering::Relaxed);
958 self.cached_bytes.store(0, Ordering::Relaxed);
959 }
960}
961
962impl Default for ContentCacheBudget {
963 fn default() -> Self {
964 Self::new_for_repo(30_000)
965 }
966}