Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ signal-hook-registry = "1.4"
zlob = { version = "=1.6.0-dev.7" }

mlua = { version = "0.11.1", features = ["module", "luajit"] }
neo_frizbee = { version = "0.10.3", features = ["match_end_col"] }
neo_frizbee = { version = "0.10.4", features = ["match_end_col"] }
notify = { version = "9.0.0-rc.3" }
notify-debouncer-full = { package = "fff-notify-debouncer-full", version = "0.9.4" }
once_cell = "1.20.2"
Expand Down
45 changes: 35 additions & 10 deletions crates/fff-core/src/score.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
constraints::apply_constraints,
git::is_modified_status,
path_utils::calculate_distance_penalty,
simd_path::ArenaPtr,
simd_path::{ArenaPtr, MAX_PATH_CHUNKS},
sort_buffer::{sort_by_key_with_buffer, sort_with_buffer},
types::{DirItem, FileItem, Score, ScoringContext},
};
Expand Down Expand Up @@ -32,7 +32,7 @@ impl<'a> FileItems<'a> {
fn resolve_file_chunks(
file: &FileItem,
arena: ArenaPtr,
buf: &mut [*const u8; 32],
buf: &mut [*const u8; MAX_PATH_CHUNKS],
) -> Option<(usize, u16)> {
if file.is_deleted() {
return None;
Expand Down Expand Up @@ -60,15 +60,15 @@ fn match_fuzzy_parts(
return vec![];
}

let resolve = |file: &FileItem, buf: &mut [*const u8; 32]| -> Option<(usize, u16)> {
resolve_file_chunks(file, arena, buf)
};
let resolve = |file: &FileItem,
buf: &mut [*const u8; MAX_PATH_CHUNKS]|
-> Option<(usize, u16)> { resolve_file_chunks(file, arena, buf) };

// because we reassemble the vec of reference we have to use a different type
// to narrow down the [&FileItem] which would be resolved by frizbee as &&
let resolve_ref = |file: &&FileItem, buf: &mut [*const u8; 32]| -> Option<(usize, u16)> {
resolve_file_chunks(file, arena, buf)
};
let resolve_ref = |file: &&FileItem,
buf: &mut [*const u8; MAX_PATH_CHUNKS]|
-> Option<(usize, u16)> { resolve_file_chunks(file, arena, buf) };

let first_part_matches = match working_files {
FileItems::All(files) => neo_frizbee::match_list_parallel_resolved(
Expand Down Expand Up @@ -173,7 +173,7 @@ pub(crate) fn fuzzy_match_and_score_files<'a>(
fn resolve_dir_chunks(
dir: &DirItem,
arena: ArenaPtr,
buf: &mut [*const u8; 32],
buf: &mut [*const u8; MAX_PATH_CHUNKS],
) -> Option<(usize, u16)> {
let ptrs = dir.path.resolve_ptrs(arena, buf);
Some((ptrs.len(), dir.path.byte_len))
Expand All @@ -199,7 +199,7 @@ fn match_fuzzy_parts_dirs(
}

let resolve_chunks_for_frizbee =
|dir: &&DirItem, buf: &mut [*const u8; 32]| -> Option<(usize, u16)> {
|dir: &&DirItem, buf: &mut [*const u8; MAX_PATH_CHUNKS]| -> Option<(usize, u16)> {
resolve_dir_chunks(dir, arena, buf)
};

Expand Down Expand Up @@ -1315,6 +1315,31 @@ mod filename_bonus_tests {
);
}

/// Regression: PR #652 / field panic in pi-fff v0.9.6.
/// A path >512 bytes (but within PATH_MAX) overflows the fixed
/// `[*const u8; 32]` chunk-pointer buffer during scoring and panics with
/// "index out of bounds: the len is 32 but the index is 32".
#[test]
fn test_path_longer_than_512_bytes_does_not_panic_and_matches() {
let mut long_path = String::new();
while long_path.len() < 600 {
long_path.push_str("deeply_nested_directory_segment/");
}
long_path.push_str("needle_file.rs");
assert!(long_path.len() > 512 && long_path.len() < crate::simd_path::PATH_BUF_SIZE);

let (files, arena) = make_files(&[long_path.as_str(), "src/other.rs"]);

// Panics here on unfixed code: frizbee resolves chunk ptrs per file.
let results = search(&files, "needle", arena);

assert!(
results.iter().any(|(p, _)| p == &long_path),
"filename at the tail of a >512-byte path must still match, got: {:?}",
results.iter().map(|(p, _)| p).collect::<Vec<_>>()
);
}

#[test]
fn test_single_path_matching() {
let path = "core_workflow_service/kafka_event_consumer/src/ai_part_extraction_request/ai_part_extraction_request_handler.rs";
Expand Down
52 changes: 44 additions & 8 deletions crates/fff-core/src/simd_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ impl std::fmt::Debug for SimdChunk {

pub use crate::constants::PATH_BUF_SIZE;

/// Chunk pointer capacity needed for the longest path the platform allows.
pub(crate) const MAX_PATH_CHUNKS: usize = PATH_BUF_SIZE.div_ceil(SIMD_CHUNK_BYTES);

/// Indices into a shared `SimdChunk` arena representing a file path.
///
/// All read methods require an explicit `arena_base` pointer from the owning
Expand Down Expand Up @@ -98,14 +101,10 @@ impl ChunkedString {
}

#[inline]
pub fn resolve_ptrs<'a>(
&self,
arena: ArenaPtr,
buf: &'a mut [*const u8; 32],
) -> &'a [*const u8] {
let count = self.indices.len();
pub fn resolve_ptrs<'a>(&self, arena: ArenaPtr, buf: &'a mut [*const u8]) -> &'a [*const u8] {
let count = self.indices.len().min(buf.len());
let base = arena.as_ptr();
for (i, &idx) in self.indices.iter().enumerate() {
for (i, &idx) in self.indices[..count].iter().enumerate() {
buf[i] = unsafe { base.add(idx as usize * SIMD_CHUNK_BYTES) };
}
&buf[..count]
Expand Down Expand Up @@ -460,7 +459,7 @@ mod tests {
let arena = store.as_arena_ptr();
let cs = &strings[0];

let mut ptrs = [std::ptr::null::<u8>(); 32];
let mut ptrs = [std::ptr::null::<u8>(); MAX_PATH_CHUNKS];
let resolved = cs.resolve_ptrs(arena, &mut ptrs);
assert_eq!(resolved.len(), 2); // 25 bytes = 2 chunks

Expand All @@ -478,6 +477,43 @@ mod tests {
);
}

#[test]
fn test_resolve_ptrs_path_exceeding_512_bytes() {
// Regression: a fixed 32-ptr buffer covered only 512 bytes while
// PATH_BUF_SIZE (libc::PATH_MAX) allows longer paths, panicking with
// "index out of bounds: the len is 32 but the index is 32"
let mut path = String::new();
while path.len() < 600 {
path.push_str("deeply_nested_directory_segment/");
}
path.push_str("needle_file.rs");
assert!(path.len() > 512 && path.len() < PATH_BUF_SIZE);

let (store, strings, _files) = build_test_store(&[path.as_str()]);
let arena = store.as_arena_ptr();
let cs = &strings[0];
assert!(cs.chunk_count() > 32, "path must span more than 32 chunks");

let mut ptrs = [std::ptr::null::<u8>(); MAX_PATH_CHUNKS];
let resolved = cs.resolve_ptrs(arena, &mut ptrs);

// Truncation is not acceptable either: it silently drops the tail of
// the path (including the filename here) from fuzzy matching.
assert_eq!(
resolved.len(),
cs.chunk_count(),
"resolve_ptrs must resolve every chunk of a PATH_MAX-legal path"
);

let total = cs.byte_len as usize;
let mut reconstructed = Vec::with_capacity(total);
for (i, &ptr) in resolved.iter().enumerate() {
let take = SIMD_CHUNK_BYTES.min(total - i * SIMD_CHUNK_BYTES);
reconstructed.extend_from_slice(unsafe { std::slice::from_raw_parts(ptr, take) });
}
assert_eq!(std::str::from_utf8(&reconstructed).unwrap(), path);
}

#[test]
fn test_filename_cow_mid_chunk() {
let (store, strings, _files) = build_test_store(&["src/components/Button.tsx"]);
Expand Down
Loading