From e485495aac8936e4128e02e39adf9699d1a0066c Mon Sep 17 00:00:00 2001 From: mola <2249464964@qq.com> Date: Sun, 18 Oct 2026 21:44:14 +0800 Subject: [PATCH] fix(core): keep listed entries when the completion stat returns NotFound --- core/core/src/layers/complete.rs | 30 ++++++++++++++++-------------- core/services/fs/src/core.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/core/core/src/layers/complete.rs b/core/core/src/layers/complete.rs index 5e9a4b484ccb..e75e1f790231 100644 --- a/core/core/src/layers/complete.rs +++ b/core/core/src/layers/complete.rs @@ -238,7 +238,15 @@ impl CompleteLister { } .into(); - let stat_metadata = self.srv.stat(&self.ctx, &path, op).await?.into_metadata(); + let stat_metadata = match self.srv.stat(&self.ctx, &path, op).await { + Ok(rp) => rp.into_metadata(), + // The entry comes from the service's own listing, so a stat that + // cannot see its target — a dangling symlink in fs, whose stat + // follows the link — cannot refine the mode, but it does not mean + // the entry is gone. Keep the entry as listed. + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(entry), + Err(err) => return Err(err), + }; if stat_metadata.mode() == EntryMode::Unknown { return Ok(entry); } @@ -256,21 +264,15 @@ impl CompleteLister { impl oio::List for CompleteLister { async fn next(&mut self) -> Result> { - loop { - let Some(entry) = self.inner.next().await? else { - return Ok(None); - }; - - if entry.mode() != EntryMode::Unknown || entry.metadata().is_deleted() { - return Ok(Some(entry)); - } + let Some(entry) = self.inner.next().await? else { + return Ok(None); + }; - match self.complete_unknown_entry_mode(entry).await { - Ok(entry) => return Ok(Some(entry)), - Err(err) if err.kind() == ErrorKind::NotFound => continue, - Err(err) => return Err(err), - } + if entry.mode() != EntryMode::Unknown || entry.metadata().is_deleted() { + return Ok(Some(entry)); } + + Ok(Some(self.complete_unknown_entry_mode(entry).await?)) } } diff --git a/core/services/fs/src/core.rs b/core/services/fs/src/core.rs index 382d3560ebd1..8c970d1415cb 100644 --- a/core/services/fs/src/core.rs +++ b/core/services/fs/src/core.rs @@ -443,6 +443,33 @@ mod tests { assert_eq!(std::fs::read(&outside).unwrap(), b"content"); } + #[cfg(unix)] + #[tokio::test] + async fn test_list_includes_dangling_symlink() { + use opendal_core::Operator; + + let temp_dir = tempfile::TempDir::new().unwrap(); + let root = temp_dir.path(); + + std::fs::write(root.join("real"), b"data").unwrap(); + std::os::unix::fs::symlink("/nonexistent-target", root.join("linky")).unwrap(); + std::os::unix::fs::symlink(root.join("real"), root.join("valid-link")).unwrap(); + + let op = Operator::new(crate::Fs::default().root(root.to_str().unwrap())).unwrap(); + let entries = op.list("/").await.unwrap(); + let modes: std::collections::HashMap = entries + .iter() + .map(|e| (e.path().to_string(), e.metadata().mode())) + .collect(); + + assert_eq!( + modes.get("linky"), + Some(&EntryMode::Unknown), + "dangling symlink must be listed as Unknown: {modes:?}" + ); + assert_eq!(modes.get("real"), Some(&EntryMode::FILE)); + } + #[tokio::test] async fn test_delete_iter_rejects_parent_path_outside_root() { use opendal_core::Operator;