feat(scanner): add option not to follow symlinks (#5325)

Add follow_symlinks option

Settings this to false can fix hanging on symlinks to slow/inaccessible
filesystems.
This commit is contained in:
Ohad Lutzky 2023-11-02 08:01:09 +00:00 committed by GitHub
parent ca5851d155
commit 7b851fc30e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 13 deletions

View File

@ -1794,6 +1794,10 @@
"default": true,
"type": "boolean"
},
"follow_symlinks": {
"default": true,
"type": "boolean"
},
"palette": {
"type": [
"string",

View File

@ -219,6 +219,14 @@ This is the list of prompt-wide configuration options.
| `add_newline` | `true` | Inserts blank line between shell prompts. |
| `palette` | `''` | Sets which color palette from `palettes` to use. |
| `palettes` | `{}` | Collection of color palettes that assign [colors](/advanced-config/#style-strings) to user-defined names. Note that color palettes cannot reference their own color definitions. |
| `follow_symlinks` | `true` | Follows symlinks to check if they're directories; used in modules such as git. |
::: tip
If you have symlinks to networked filesystems, consider setting
`follow_symlinks` to `false`.
:::
### Example

View File

@ -18,6 +18,7 @@ pub struct StarshipRootConfig {
pub scan_timeout: u64,
pub command_timeout: u64,
pub add_newline: bool,
pub follow_symlinks: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub palette: Option<String>,
pub palettes: HashMap<String, Palette>,
@ -134,6 +135,7 @@ impl Default for StarshipRootConfig {
scan_timeout: 30,
command_timeout: 500,
add_newline: true,
follow_symlinks: true,
palette: None,
palettes: HashMap::default(),
}

View File

@ -358,7 +358,11 @@ impl<'a> Context<'a> {
pub fn dir_contents(&self) -> Result<&DirContents, std::io::Error> {
self.dir_contents.get_or_try_init(|| {
let timeout = self.root_config.scan_timeout;
DirContents::from_path_with_timeout(&self.current_dir, Duration::from_millis(timeout))
DirContents::from_path_with_timeout(
&self.current_dir,
Duration::from_millis(timeout),
self.root_config.follow_symlinks,
)
})
}
@ -479,11 +483,15 @@ pub struct DirContents {
impl DirContents {
#[cfg(test)]
fn from_path(base: &Path) -> Result<Self, std::io::Error> {
Self::from_path_with_timeout(base, Duration::from_secs(30))
fn from_path(base: &Path, follow_symlinks: bool) -> Result<Self, std::io::Error> {
Self::from_path_with_timeout(base, Duration::from_secs(30), follow_symlinks)
}
fn from_path_with_timeout(base: &Path, timeout: Duration) -> Result<Self, std::io::Error> {
fn from_path_with_timeout(
base: &Path,
timeout: Duration,
follow_symlinks: bool,
) -> Result<Self, std::io::Error> {
let start = Instant::now();
let mut folders: HashSet<PathBuf> = HashSet::new();
@ -501,7 +509,15 @@ impl DirContents {
.filter_map(|(_, entry)| entry.ok())
.for_each(|entry| {
let path = PathBuf::from(entry.path().strip_prefix(base).unwrap());
if entry.path().is_dir() {
let is_dir = match follow_symlinks {
true => entry.path().is_dir(),
false => fs::symlink_metadata(entry.path())
.map(|m| m.is_dir())
.unwrap_or(false),
};
if is_dir {
folders.insert(path);
} else {
if !path.to_string_lossy().starts_with('.') {
@ -897,10 +913,63 @@ mod tests {
Ok(dir)
}
#[test]
fn test_scan_dir_no_symlinks() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::symlink;
#[cfg(target_os = "windows")]
use std::os::windows::fs::symlink_dir as symlink;
let d = testdir(&["file"])?;
fs::create_dir(d.path().join("folder"))?;
symlink(d.path().join("folder"), d.path().join("link_to_folder"))?;
symlink(d.path().join("file"), d.path().join("link_to_file"))?;
let dc_following_symlinks = DirContents::from_path(d.path(), true)?;
assert!(ScanDir {
dir_contents: &dc_following_symlinks,
files: &["link_to_file"],
extensions: &[],
folders: &[],
}
.is_match());
assert!(ScanDir {
dir_contents: &dc_following_symlinks,
files: &[],
extensions: &[],
folders: &["link_to_folder"],
}
.is_match());
let dc_not_following_symlinks = DirContents::from_path(d.path(), false)?;
assert!(ScanDir {
dir_contents: &dc_not_following_symlinks,
files: &["link_to_file"],
extensions: &[],
folders: &[],
}
.is_match());
assert!(!ScanDir {
dir_contents: &dc_not_following_symlinks,
files: &[],
extensions: &[],
folders: &["link_to_folder"],
}
.is_match());
Ok(())
}
#[test]
fn test_scan_dir() -> Result<(), Box<dyn std::error::Error>> {
let empty = testdir(&[])?;
let empty_dc = DirContents::from_path(empty.path())?;
let follow_symlinks = true;
let empty_dc = DirContents::from_path(empty.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &empty_dc,
@ -912,7 +981,7 @@ mod tests {
empty.close()?;
let rust = testdir(&["README.md", "Cargo.toml", "src/main.rs"])?;
let rust_dc = DirContents::from_path(rust.path())?;
let rust_dc = DirContents::from_path(rust.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &rust_dc,
files: &["package.json"],
@ -923,7 +992,7 @@ mod tests {
rust.close()?;
let java = testdir(&["README.md", "src/com/test/Main.java", "pom.xml"])?;
let java_dc = DirContents::from_path(java.path())?;
let java_dc = DirContents::from_path(java.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &java_dc,
files: &["package.json"],
@ -934,7 +1003,7 @@ mod tests {
java.close()?;
let node = testdir(&["README.md", "node_modules/lodash/main.js", "package.json"])?;
let node_dc = DirContents::from_path(node.path())?;
let node_dc = DirContents::from_path(node.path(), follow_symlinks)?;
assert!(ScanDir {
dir_contents: &node_dc,
files: &["package.json"],
@ -945,7 +1014,7 @@ mod tests {
node.close()?;
let tarballs = testdir(&["foo.tgz", "foo.tar.gz"])?;
let tarballs_dc = DirContents::from_path(tarballs.path())?;
let tarballs_dc = DirContents::from_path(tarballs.path(), follow_symlinks)?;
assert!(ScanDir {
dir_contents: &tarballs_dc,
files: &[],
@ -956,7 +1025,7 @@ mod tests {
tarballs.close()?;
let dont_match_ext = testdir(&["foo.js", "foo.ts"])?;
let dont_match_ext_dc = DirContents::from_path(dont_match_ext.path())?;
let dont_match_ext_dc = DirContents::from_path(dont_match_ext.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &dont_match_ext_dc,
files: &[],
@ -967,7 +1036,7 @@ mod tests {
dont_match_ext.close()?;
let dont_match_file = testdir(&["goodfile", "evilfile"])?;
let dont_match_file_dc = DirContents::from_path(dont_match_file.path())?;
let dont_match_file_dc = DirContents::from_path(dont_match_file.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &dont_match_file_dc,
files: &["goodfile", "!notfound", "!evilfile"],
@ -978,7 +1047,8 @@ mod tests {
dont_match_file.close()?;
let dont_match_folder = testdir(&["gooddir/somefile", "evildir/somefile"])?;
let dont_match_folder_dc = DirContents::from_path(dont_match_folder.path())?;
let dont_match_folder_dc =
DirContents::from_path(dont_match_folder.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &dont_match_folder_dc,
files: &[],