This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch xuanwo/ls-return-content-length
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit d2516e204957d560e910673a5177df248178298f
Author: Xuanwo <[email protected]>
AuthorDate: Mon Feb 23 06:16:13 2026 +0800

    fix(core): enforce list file content length in complete layer
---
 core/core/src/layers/complete.rs  | 61 +++++++++++++++++++++++++++++++++++++--
 core/core/src/raw/oio/entry.rs    | 10 +++++++
 core/core/src/types/metadata.rs   |  5 ++++
 core/tests/behavior/async_list.rs |  3 +-
 4 files changed, 75 insertions(+), 4 deletions(-)

diff --git a/core/core/src/layers/complete.rs b/core/core/src/layers/complete.rs
index d9cf1a403..9c845b153 100644
--- a/core/core/src/layers/complete.rs
+++ b/core/core/src/layers/complete.rs
@@ -57,7 +57,7 @@ impl<A: Access> LayeredAccess for CompleteAccessor<A> {
     type Inner = A;
     type Reader = CompleteReader<A::Reader>;
     type Writer = CompleteWriter<A::Writer>;
-    type Lister = A::Lister;
+    type Lister = CompleteLister<A>;
     type Deleter = A::Deleter;
 
     fn inner(&self) -> &Self::Inner {
@@ -95,7 +95,9 @@ impl<A: Access> LayeredAccess for CompleteAccessor<A> {
     }
 
     async fn list(&self, path: &str, args: OpList) -> Result<(RpList, 
Self::Lister)> {
-        self.inner.list(path, args).await
+        let (rp, lister) = self.inner.list(path, args).await?;
+        let lister = CompleteLister::new(self.inner.clone(), 
self.info.clone(), lister);
+        Ok((rp, lister))
     }
 
     async fn presign(&self, path: &str, args: OpPresign) -> Result<RpPresign> {
@@ -103,6 +105,61 @@ impl<A: Access> LayeredAccess for CompleteAccessor<A> {
     }
 }
 
+pub struct CompleteLister<A: Access> {
+    inner: A::Lister,
+    acc: Arc<A>,
+    info: Arc<AccessorInfo>,
+}
+
+impl<A: Access> CompleteLister<A> {
+    fn new(acc: Arc<A>, info: Arc<AccessorInfo>, inner: A::Lister) -> Self {
+        Self { inner, acc, info }
+    }
+
+    async fn ensure_file_content_length(&self, mut entry: oio::Entry) -> 
Result<oio::Entry> {
+        if !entry.mode().is_file()
+            || entry.metadata().is_deleted()
+            || entry.metadata().has_content_length()
+        {
+            return Ok(entry);
+        }
+
+        let path = entry.path().to_string();
+        let version = entry.metadata().version().map(str::to_owned);
+        let mut op = OpStat::new();
+        if let Some(version) = version.as_deref() {
+            op = op.with_version(version);
+        }
+
+        let stat_metadata = self.acc.stat(&path, op).await?.into_metadata();
+        if !stat_metadata.has_content_length() {
+            return Err(Error::new(
+                ErrorKind::Unexpected,
+                "content length is required for list file entries",
+            )
+            .with_operation("CompleteLister::ensure_file_content_length")
+            .with_context("service", self.info.scheme().to_string())
+            .with_context("path", path));
+        }
+
+        entry
+            .metadata_mut()
+            .set_content_length(stat_metadata.content_length());
+        Ok(entry)
+    }
+}
+
+impl<A: Access> oio::List for CompleteLister<A> {
+    async fn next(&mut self) -> Result<Option<oio::Entry>> {
+        let Some(entry) = self.inner.next().await? else {
+            return Ok(None);
+        };
+
+        let entry = self.ensure_file_content_length(entry).await?;
+        Ok(Some(entry))
+    }
+}
+
 pub struct CompleteReader<R> {
     inner: R,
     size: Option<u64>,
diff --git a/core/core/src/raw/oio/entry.rs b/core/core/src/raw/oio/entry.rs
index 70320300b..34d64bc7e 100644
--- a/core/core/src/raw/oio/entry.rs
+++ b/core/core/src/raw/oio/entry.rs
@@ -86,4 +86,14 @@ impl Entry {
     pub(crate) fn into_entry(self) -> crate::Entry {
         crate::Entry::new(self.path, self.meta)
     }
+
+    /// Get metadata of entry.
+    pub(crate) fn metadata(&self) -> &Metadata {
+        &self.meta
+    }
+
+    /// Get mutable metadata of entry.
+    pub(crate) fn metadata_mut(&mut self) -> &mut Metadata {
+        &mut self.meta
+    }
 }
diff --git a/core/core/src/types/metadata.rs b/core/core/src/types/metadata.rs
index 901979dc4..fe18a5d30 100644
--- a/core/core/src/types/metadata.rs
+++ b/core/core/src/types/metadata.rs
@@ -222,6 +222,11 @@ impl Metadata {
         self.content_length.unwrap_or_default()
     }
 
+    /// Returns `true` if this metadata contains an explicit content length.
+    pub(crate) fn has_content_length(&self) -> bool {
+        self.content_length.is_some()
+    }
+
     /// Set content length of this entry.
     pub fn set_content_length(&mut self, v: u64) -> &mut Self {
         self.content_length = Some(v);
diff --git a/core/tests/behavior/async_list.rs 
b/core/tests/behavior/async_list.rs
index 2be5f0d0d..fab890280 100644
--- a/core/tests/behavior/async_list.rs
+++ b/core/tests/behavior/async_list.rs
@@ -79,10 +79,9 @@ pub async fn test_list_dir(op: Operator) -> Result<()> {
     let mut obs = op.lister(&format!("{parent}/")).await?;
     let mut found = false;
     while let Some(de) = obs.try_next().await? {
-        let meta = op.stat(de.path()).await?;
         if de.path() == path {
+            let meta = de.metadata();
             assert_eq!(meta.mode(), EntryMode::FILE);
-
             assert_eq!(meta.content_length(), size as u64);
 
             found = true

Reply via email to