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
34 changes: 34 additions & 0 deletions crates/fff-core/src/walk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,40 @@ mod tests {
assert!(!names.iter().any(|n| n.contains("node_modules")));
}

// Stow-style file symlinks pointing outside the root must be indexed with
// follow_symlinks on, with the target's metadata (#912).
#[cfg(unix)]
#[test]
fn follows_file_symlinks_outside_root() {
let dir = tempfile::tempdir().unwrap();
let outside = dir.path().join("dotfiles");
let root = dir.path().join("config");
fs::create_dir_all(outside.join("nested")).unwrap();
fs::create_dir(&root).unwrap();
fs::write(outside.join("sesh.toml"), "0123456789").unwrap();
fs::write(outside.join("nested/inner.toml"), "x").unwrap();
fs::write(root.join("projects.toml"), "x").unwrap();
std::os::unix::fs::symlink("../dotfiles/sesh.toml", root.join("sesh.toml")).unwrap();
std::os::unix::fs::symlink("../dotfiles/nested", root.join("linked")).unwrap();
std::os::unix::fs::symlink("../dotfiles/missing", root.join("broken")).unwrap();

let counter = Arc::new(AtomicUsize::new(0));
let out = walk_collect_files(&root, false, true, 1, &counter).unwrap();
let mut names: Vec<String> = out.pairs.iter().map(|(_, rel)| rel.clone()).collect();
names.sort();

assert_eq!(names, ["linked/inner.toml", "projects.toml", "sesh.toml"]);
assert_eq!(counter.load(Ordering::Relaxed), names.len());

let (item, rel) = out.pairs.iter().find(|(_, r)| r == "sesh.toml").unwrap();
assert_eq!(item.size, 10);
assert_eq!(&rel[item.filename_offset_in_relative_path()..], "sesh.toml");

let out = walk_collect_files(&root, false, false, 1, &counter).unwrap();
let names: Vec<String> = out.pairs.into_iter().map(|(_, rel)| rel).collect();
assert_eq!(names, ["projects.toml"]);
}

// Only the zlob backend surfaces reusable ignore rules; they must match
// the same tree the walk respected.
#[cfg(feature = "zlob")]
Expand Down
55 changes: 40 additions & 15 deletions crates/fff-core/src/walk/zlob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use parking_lot::Mutex;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::SystemTime;
use zlob::walk::{WalkBuilder, WalkFlags, WalkMetadata, WalkState};

const PROGRESS_STEP: usize = 13;
Expand Down Expand Up @@ -51,7 +52,16 @@ pub(crate) fn walk_collect_files(
let collected = Mutex::new((Vec::new(), Vec::new()));

let outcome = match builder.run(|entry| {
if !entry.is_file() {
// Followed links keep the Symlink kind and lstat metadata, so stat
// the target to index the ones resolving to files (#912).
let target = if entry.is_file() {
None
} else if follow_symlinks && entry.is_symlink() {
match std::fs::metadata(entry.path()) {
Ok(meta) if meta.is_file() => Some(meta),
_ => return WalkState::Continue,
}
} else {
// unlike ripgrep walker zlob doesnt show .git files
if entry.is_dir() {
let relative_path = entry.relative_path_lossy();
Expand All @@ -63,26 +73,41 @@ pub(crate) fn walk_collect_files(
}

return WalkState::Continue;
}

// `basename()` returns `&str` for files only.
let basename = entry.basename().unwrap_or("");
let is_binary = is_known_binary_extension_basename(basename);

let size = entry.size().unwrap_or(0);
// zlob reports mtime in ns since the Unix epoch; FileItem wants secs.
let modified = entry
.modified_ns()
.map(|ns| (ns / 1_000_000_000).max(0) as u64)
.unwrap_or(0);
};

// Lossy pair: the offset must index the decoded string, not the raw
// bytes, or it lands inside a U+FFFD on invalid-UTF-8 names (#799).
let basename_offset = entry.basename_offset_in_relative_lossy() as u16;
let basename_offset = entry.basename_offset_in_relative_lossy();
// zlob emits '/'-separated relative paths, which is fff's canonical
// internal form on every platform — store them verbatim.
let relative_path = entry.relative_path_lossy().into_owned();
let item = FileItem::new_raw(basename_offset, size, modified, None, is_binary);

// `basename()` returns `&str` for files only.
let basename = match target {
None => entry.basename().unwrap_or(""),
Some(_) => &relative_path[basename_offset..],
};
let is_binary = is_known_binary_extension_basename(basename);

let (size, modified) = match target {
Some(meta) => (
meta.len(),
meta.modified()
.ok()
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.map_or(0, |d| d.as_secs()),
),
// zlob reports mtime in ns since the Unix epoch; FileItem wants secs.
None => (
entry.size().unwrap_or(0),
entry
.modified_ns()
.map(|ns| (ns / 1_000_000_000).max(0) as u64)
.unwrap_or(0),
),
};

let item = FileItem::new_raw(basename_offset as u16, size, modified, None, is_binary);

let mut guard = collected.lock();
guard.0.push((item, relative_path));
Expand Down
Loading