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

dentiny pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 0fe36fdd3 feat(binding/go): expose all metata fields to golang (#7646)
0fe36fdd3 is described below

commit 0fe36fdd3ca481a17d22e16046afbacff611e2be
Author: dentiny <[email protected]>
AuthorDate: Sat May 30 14:10:16 2026 -0700

    feat(binding/go): expose all metata fields to golang (#7646)
---
 bindings/c/include/opendal.h                  | 142 +++++++---
 bindings/c/src/lib.rs                         |   2 +
 bindings/c/src/metadata.rs                    | 235 ++++++++++++++---
 bindings/go/metadata.go                       | 300 ++++++++++++++++++++-
 bindings/go/string_ownership_test.go          | 358 ++++++++++++++++++++++++++
 bindings/go/tests/behavior_tests/stat_test.go |  65 +++++
 bindings/go/types.go                          |   7 +
 7 files changed, 1034 insertions(+), 75 deletions(-)

diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h
index 493c62c12..5a040ef92 100644
--- a/bindings/c/include/opendal.h
+++ b/bindings/c/include/opendal.h
@@ -220,6 +220,31 @@ typedef struct opendal_metadata {
   void *inner;
 } opendal_metadata;
 
+/**
+ * \brief User metadata associated with a **path**.
+ */
+typedef struct opendal_metadata_user_metadata {
+  /**
+   * The pointer to the user metadata in the Rust code.
+   * Only touch this on judging whether it is NULL.
+   */
+  void *inner;
+} opendal_metadata_user_metadata;
+
+/**
+ * \brief A user metadata key-value pair.
+ */
+typedef struct opendal_metadata_user_metadata_pair {
+  /**
+   * The key of the user metadata.
+   */
+  const char *key;
+  /**
+   * The value of the user metadata.
+   */
+  const char *value;
+} opendal_metadata_user_metadata_pair;
+
 /**
  * \brief Used to access almost all OpenDAL APIs. It represents an
  * operator that provides the unified interfaces provided by OpenDAL.
@@ -762,55 +787,93 @@ void opendal_operator_layers_free(struct 
opendal_operator_layers *ptr);
  */
 void opendal_metadata_free(struct opendal_metadata *ptr);
 
+/**
+ * \brief Return mode of the metadata: 0 for unknown, 1 for file, and 2 for 
dir.
+ */
+uint8_t opendal_metadata_mode(const struct opendal_metadata *self);
+
 /**
  * \brief Return the content_length of the metadata
- *
- * # Example
- * ```C
- * // ... previously you wrote "Hello, World!" to path "/testpath"
- * opendal_result_stat s = opendal_operator_stat(op, "/testpath");
- * assert(s.error == NULL);
- *
- * opendal_metadata *meta = s.meta;
- * assert(opendal_metadata_content_length(meta) == 13);
- * ```
  */
 uint64_t opendal_metadata_content_length(const struct opendal_metadata *self);
 
 /**
  * \brief Return whether the path represents a file
- *
- * # Example
- * ```C
- * // ... previously you wrote "Hello, World!" to path "/testpath"
- * opendal_result_stat s = opendal_operator_stat(op, "/testpath");
- * assert(s.error == NULL);
- *
- * opendal_metadata *meta = s.meta;
- * assert(opendal_metadata_is_file(meta));
- * ```
  */
 bool opendal_metadata_is_file(const struct opendal_metadata *self);
 
 /**
  * \brief Return whether the path represents a directory
+ */
+bool opendal_metadata_is_dir(const struct opendal_metadata *self);
+
+/**
+ * \brief Return whether this metadata is current.
  *
- * # Example
- * ```C
- * // ... previously you wrote "Hello, World!" to path "/testpath"
- * opendal_result_stat s = opendal_operator_stat(op, "/testpath");
- * assert(s.error == NULL);
+ * Returns 1 for current, 0 for not current, and 2 if unknown.
+ */
+uint8_t opendal_metadata_is_current(const struct opendal_metadata *self);
+
+/**
+ * \brief Return whether this metadata is deleted.
+ */
+bool opendal_metadata_is_deleted(const struct opendal_metadata *self);
+
+/**
+ * \brief Return the cache control of the metadata.
  *
- * opendal_metadata *meta = s.meta;
+ * \note: The string is on heap, free it with opendal_string_free().
+ */
+char *opendal_metadata_cache_control(const struct opendal_metadata *self);
+
+/**
+ * \brief Return the content disposition of the metadata.
  *
- * // this is not a directory
- * assert(!opendal_metadata_is_dir(meta));
- * ```
+ * \note: The string is on heap, free it with opendal_string_free().
+ */
+char *opendal_metadata_content_disposition(const struct opendal_metadata 
*self);
+
+/**
+ * \brief Return the content md5 of the metadata.
  *
- * \todo This is not a very clear example. A clearer example will be added
- * after we support opendal_operator_mkdir()
+ * \note: The string is on heap, free it with opendal_string_free().
  */
-bool opendal_metadata_is_dir(const struct opendal_metadata *self);
+char *opendal_metadata_content_md5(const struct opendal_metadata *self);
+
+/**
+ * \brief Return the content type of the metadata.
+ *
+ * \note: The string is on heap, free it with opendal_string_free().
+ */
+char *opendal_metadata_content_type(const struct opendal_metadata *self);
+
+/**
+ * \brief Return the content encoding of the metadata.
+ *
+ * \note: The string is on heap, free it with opendal_string_free().
+ */
+char *opendal_metadata_content_encoding(const struct opendal_metadata *self);
+
+/**
+ * \brief Return the etag of the metadata.
+ *
+ * \note: The string is on heap, free it with opendal_string_free().
+ */
+char *opendal_metadata_etag(const struct opendal_metadata *self);
+
+/**
+ * \brief Return the version of the metadata.
+ *
+ * \note: The string is on heap, free it with opendal_string_free().
+ */
+char *opendal_metadata_version(const struct opendal_metadata *self);
+
+/**
+ * \brief Return the user metadata of the metadata.
+ *
+ * \note: The returned user metadata is on heap, free it with 
opendal_metadata_user_metadata_free().
+ */
+struct opendal_metadata_user_metadata 
*opendal_metadata_get_user_metadata(const struct opendal_metadata *self);
 
 /**
  * \brief Return the last_modified of the metadata, in milliseconds
@@ -827,6 +890,21 @@ bool opendal_metadata_is_dir(const struct opendal_metadata 
*self);
  */
 int64_t opendal_metadata_last_modified_ms(const struct opendal_metadata *self);
 
+/**
+ * \brief Return the key-value pairs of the user metadata.
+ */
+const struct opendal_metadata_user_metadata_pair 
*opendal_metadata_user_metadata_pairs(const struct 
opendal_metadata_user_metadata *metadata);
+
+/**
+ * \brief Return the number of key-value pairs in the user metadata.
+ */
+uintptr_t opendal_metadata_user_metadata_len(const struct 
opendal_metadata_user_metadata *metadata);
+
+/**
+ * \brief Free the user metadata returned by opendal_metadata_user_metadata.
+ */
+void opendal_metadata_user_metadata_free(struct opendal_metadata_user_metadata 
*metadata);
+
 /**
  * \brief Free the heap-allocated operator pointed by opendal_operator.
  *
diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs
index 89fd461a3..5592288bd 100644
--- a/bindings/c/src/lib.rs
+++ b/bindings/c/src/lib.rs
@@ -44,6 +44,8 @@ pub use layer::opendal_operator_layers;
 
 mod metadata;
 pub use metadata::opendal_metadata;
+pub use metadata::opendal_metadata_user_metadata;
+pub use metadata::opendal_metadata_user_metadata_pair;
 
 mod operator;
 pub use operator::opendal_operator;
diff --git a/bindings/c/src/metadata.rs b/bindings/c/src/metadata.rs
index 8ace23f38..fe0b4254e 100644
--- a/bindings/c/src/metadata.rs
+++ b/bindings/c/src/metadata.rs
@@ -16,7 +16,62 @@
 // under the License.
 
 use ::opendal as core;
-use std::ffi::c_void;
+use std::collections::HashMap;
+use std::ffi::{c_char, c_void, CString};
+use std::ptr;
+
+/// \brief A user metadata key-value pair.
+#[repr(C)]
+pub struct opendal_metadata_user_metadata_pair {
+    /// The key of the user metadata.
+    pub key: *const c_char,
+    /// The value of the user metadata.
+    pub value: *const c_char,
+}
+
+struct opendal_metadata_user_metadata_inner {
+    pairs: Vec<opendal_metadata_user_metadata_pair>,
+    #[allow(dead_code)]
+    keys: Vec<CString>,
+    #[allow(dead_code)]
+    values: Vec<CString>,
+}
+
+impl opendal_metadata_user_metadata_inner {
+    fn new(user_metadata: &HashMap<String, String>) -> Self {
+        let mut keys = Vec::with_capacity(user_metadata.len());
+        let mut values = Vec::with_capacity(user_metadata.len());
+        let mut pairs = Vec::with_capacity(user_metadata.len());
+
+        for (key, value) in user_metadata {
+            keys.push(
+                CString::new(key.as_str()).expect("user metadata key should 
not contain nul"),
+            );
+            values.push(
+                CString::new(value.as_str()).expect("user metadata value 
should not contain nul"),
+            );
+
+            pairs.push(opendal_metadata_user_metadata_pair {
+                key: keys.last().expect("key has just been pushed").as_ptr(),
+                value: values.last().expect("value has just been 
pushed").as_ptr(),
+            });
+        }
+
+        Self {
+            pairs,
+            keys,
+            values,
+        }
+    }
+}
+
+/// \brief User metadata associated with a **path**.
+#[repr(C)]
+pub struct opendal_metadata_user_metadata {
+    /// The pointer to the user metadata in the Rust code.
+    /// Only touch this on judging whether it is NULL.
+    inner: *mut c_void,
+}
 
 /// \brief Carries all metadata associated with a **path**.
 ///
@@ -62,57 +117,132 @@ impl opendal_metadata {
         }
     }
 
+    fn optional_str(v: Option<&str>) -> *mut c_char {
+        match v {
+            Some(v) => CString::new(v)
+                .expect("metadata string should not contain nul")
+                .into_raw(),
+            None => ptr::null_mut(),
+        }
+    }
+
+    /// \brief Return mode of the metadata: 0 for unknown, 1 for file, and 2 
for dir.
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_mode(&self) -> u8 {
+        match self.deref().mode() {
+            core::EntryMode::Unknown => 0,
+            core::EntryMode::FILE => 1,
+            core::EntryMode::DIR => 2,
+        }
+    }
+
     /// \brief Return the content_length of the metadata
-    ///
-    /// # Example
-    /// ```C
-    /// // ... previously you wrote "Hello, World!" to path "/testpath"
-    /// opendal_result_stat s = opendal_operator_stat(op, "/testpath");
-    /// assert(s.error == NULL);
-    ///
-    /// opendal_metadata *meta = s.meta;
-    /// assert(opendal_metadata_content_length(meta) == 13);
-    /// ```
     #[no_mangle]
     pub extern "C" fn opendal_metadata_content_length(&self) -> u64 {
         self.deref().content_length()
     }
 
     /// \brief Return whether the path represents a file
-    ///
-    /// # Example
-    /// ```C
-    /// // ... previously you wrote "Hello, World!" to path "/testpath"
-    /// opendal_result_stat s = opendal_operator_stat(op, "/testpath");
-    /// assert(s.error == NULL);
-    ///
-    /// opendal_metadata *meta = s.meta;
-    /// assert(opendal_metadata_is_file(meta));
-    /// ```
     #[no_mangle]
     pub extern "C" fn opendal_metadata_is_file(&self) -> bool {
         self.deref().is_file()
     }
 
     /// \brief Return whether the path represents a directory
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_is_dir(&self) -> bool {
+        self.deref().is_dir()
+    }
+
+    /// \brief Return whether this metadata is current.
     ///
-    /// # Example
-    /// ```C
-    /// // ... previously you wrote "Hello, World!" to path "/testpath"
-    /// opendal_result_stat s = opendal_operator_stat(op, "/testpath");
-    /// assert(s.error == NULL);
+    /// Returns 1 for current, 0 for not current, and 2 if unknown.
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_is_current(&self) -> u8 {
+        match self.deref().is_current() {
+            Some(true) => 1,
+            Some(false) => 0,
+            None => 2,
+        }
+    }
+
+    /// \brief Return whether this metadata is deleted.
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_is_deleted(&self) -> bool {
+        self.deref().is_deleted()
+    }
+
+    /// \brief Return the cache control of the metadata.
     ///
-    /// opendal_metadata *meta = s.meta;
+    /// \note: The string is on heap, free it with opendal_string_free().
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_cache_control(&self) -> *mut c_char {
+        Self::optional_str(self.deref().cache_control())
+    }
+
+    /// \brief Return the content disposition of the metadata.
     ///
-    /// // this is not a directory
-    /// assert(!opendal_metadata_is_dir(meta));
-    /// ```
+    /// \note: The string is on heap, free it with opendal_string_free().
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_content_disposition(&self) -> *mut 
c_char {
+        Self::optional_str(self.deref().content_disposition())
+    }
+
+    /// \brief Return the content md5 of the metadata.
     ///
-    /// \todo This is not a very clear example. A clearer example will be added
-    /// after we support opendal_operator_mkdir()
+    /// \note: The string is on heap, free it with opendal_string_free().
     #[no_mangle]
-    pub extern "C" fn opendal_metadata_is_dir(&self) -> bool {
-        self.deref().is_dir()
+    pub extern "C" fn opendal_metadata_content_md5(&self) -> *mut c_char {
+        Self::optional_str(self.deref().content_md5())
+    }
+
+    /// \brief Return the content type of the metadata.
+    ///
+    /// \note: The string is on heap, free it with opendal_string_free().
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_content_type(&self) -> *mut c_char {
+        Self::optional_str(self.deref().content_type())
+    }
+
+    /// \brief Return the content encoding of the metadata.
+    ///
+    /// \note: The string is on heap, free it with opendal_string_free().
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_content_encoding(&self) -> *mut c_char {
+        Self::optional_str(self.deref().content_encoding())
+    }
+
+    /// \brief Return the etag of the metadata.
+    ///
+    /// \note: The string is on heap, free it with opendal_string_free().
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_etag(&self) -> *mut c_char {
+        Self::optional_str(self.deref().etag())
+    }
+
+    /// \brief Return the version of the metadata.
+    ///
+    /// \note: The string is on heap, free it with opendal_string_free().
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_version(&self) -> *mut c_char {
+        Self::optional_str(self.deref().version())
+    }
+
+    /// \brief Return the user metadata of the metadata.
+    ///
+    /// \note: The returned user metadata is on heap, free it with 
opendal_metadata_user_metadata_free().
+    #[no_mangle]
+    pub extern "C" fn opendal_metadata_get_user_metadata(
+        &self,
+    ) -> *mut opendal_metadata_user_metadata {
+        match self.deref().user_metadata() {
+            Some(user_metadata) => 
Box::into_raw(Box::new(opendal_metadata_user_metadata {
+                inner: 
Box::into_raw(Box::new(opendal_metadata_user_metadata_inner::new(
+                    user_metadata,
+                ))) as _,
+            })),
+            None => ptr::null_mut(),
+        }
     }
 
     /// \brief Return the last_modified of the metadata, in milliseconds
@@ -135,3 +265,40 @@ impl opendal_metadata {
         }
     }
 }
+
+impl opendal_metadata_user_metadata {
+    /// \brief Return the key-value pairs of the user metadata.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_metadata_user_metadata_pairs(
+        metadata: *const Self,
+    ) -> *const opendal_metadata_user_metadata_pair {
+        if metadata.is_null() {
+            return ptr::null();
+        }
+
+        let inner = unsafe { &*((*metadata).inner as *mut 
opendal_metadata_user_metadata_inner) };
+        inner.pairs.as_ptr()
+    }
+
+    /// \brief Return the number of key-value pairs in the user metadata.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_metadata_user_metadata_len(metadata: 
*const Self) -> usize {
+        if metadata.is_null() {
+            return 0;
+        }
+
+        let inner = unsafe { &*((*metadata).inner as *mut 
opendal_metadata_user_metadata_inner) };
+        inner.pairs.len()
+    }
+
+    /// \brief Free the user metadata returned by 
opendal_metadata_user_metadata.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_metadata_user_metadata_free(metadata: 
*mut Self) {
+        if !metadata.is_null() {
+            drop(unsafe {
+                Box::from_raw((*metadata).inner as *mut 
opendal_metadata_user_metadata_inner)
+            });
+            drop(unsafe { Box::from_raw(metadata) });
+        }
+    }
+}
diff --git a/bindings/go/metadata.go b/bindings/go/metadata.go
index daff59d63..057815391 100644
--- a/bindings/go/metadata.go
+++ b/bindings/go/metadata.go
@@ -27,15 +27,55 @@ import (
        "github.com/jupiterrider/ffi"
 )
 
+// EntryMode indicates whether an entry is a file, directory, or unknown.
+type EntryMode uint8
+
+const (
+       EntryModeUnknown EntryMode = iota
+       EntryModeFile
+       EntryModeDir
+)
+
 // Metadata represents essential information about a file or directory.
 //
-// This struct contains basic attributes commonly used in file systems
+// This struct contains attributes commonly used in file systems
 // and object storage systems.
 type Metadata struct {
-       contentLength uint64
-       isFile        bool
-       isDir         bool
-       lastModified  time.Time
+       cacheControl       *string
+       contentDisposition *string
+       contentEncoding    *string
+       contentLength      uint64
+       contentMD5         *string
+       contentType        *string
+       etag               *string
+       isCurrent          *bool
+       isDeleted          bool
+       isFile             bool
+       isDir              bool
+       lastModified       time.Time
+       mode               EntryMode
+       version            *string
+       userMetadata       map[string]string
+}
+
+func optionalStringValue(v *string) (string, bool) {
+       if v == nil {
+               return "", false
+       }
+       return *v, true
+}
+
+func boolPtrFromOptionalByte(v uint8) *bool {
+       switch v {
+       case 0:
+               b := false
+               return &b
+       case 1:
+               b := true
+               return &b
+       default:
+               return nil
+       }
 }
 
 func newMetadata(ctx context.Context, inner *opendalMetadata) *Metadata {
@@ -46,16 +86,59 @@ func newMetadata(ctx context.Context, inner 
*opendalMetadata) *Metadata {
                lastModified = time.UnixMilli(ms)
        }
 
+       isCurrent := 
boolPtrFromOptionalByte(ffiMetaIsCurrent.symbol(ctx)(inner))
+
        defer ffiMetadataFree.symbol(ctx)(inner)
 
        return &Metadata{
-               contentLength: ffiMetaContentLength.symbol(ctx)(inner),
-               isFile:        ffiMetaIsFile.symbol(ctx)(inner),
-               isDir:         ffiMetaIsDir.symbol(ctx)(inner),
-               lastModified:  lastModified,
+               cacheControl:       ffiMetaCacheControl.symbol(ctx)(inner),
+               contentDisposition: 
ffiMetaContentDisposition.symbol(ctx)(inner),
+               contentEncoding:    ffiMetaContentEncoding.symbol(ctx)(inner),
+               contentLength:      ffiMetaContentLength.symbol(ctx)(inner),
+               contentMD5:         ffiMetaContentMD5.symbol(ctx)(inner),
+               contentType:        ffiMetaContentType.symbol(ctx)(inner),
+               etag:               ffiMetaEtag.symbol(ctx)(inner),
+               isCurrent:          isCurrent,
+               isDeleted:          ffiMetaIsDeleted.symbol(ctx)(inner),
+               isFile:             ffiMetaIsFile.symbol(ctx)(inner),
+               isDir:              ffiMetaIsDir.symbol(ctx)(inner),
+               lastModified:       lastModified,
+               mode:               ffiMetaMode.symbol(ctx)(inner),
+               version:            ffiMetaVersion.symbol(ctx)(inner),
+               userMetadata:       ffiMetaUserMetadata.symbol(ctx)(inner),
        }
 }
 
+// CacheControl returns the cache control of the entry.
+func (m *Metadata) CacheControl() (string, bool) {
+       return optionalStringValue(m.cacheControl)
+}
+
+// ContentDisposition returns the content disposition of the entry.
+func (m *Metadata) ContentDisposition() (string, bool) {
+       return optionalStringValue(m.contentDisposition)
+}
+
+// ContentEncoding returns the content encoding of the entry.
+func (m *Metadata) ContentEncoding() (string, bool) {
+       return optionalStringValue(m.contentEncoding)
+}
+
+// ContentMD5 returns the content MD5 of the entry.
+func (m *Metadata) ContentMD5() (string, bool) {
+       return optionalStringValue(m.contentMD5)
+}
+
+// ContentType returns the content type of the entry.
+func (m *Metadata) ContentType() (string, bool) {
+       return optionalStringValue(m.contentType)
+}
+
+// ETag returns the ETag of the entry.
+func (m *Metadata) ETag() (string, bool) {
+       return optionalStringValue(m.etag)
+}
+
 // ContentLength returns the size of the file in bytes.
 //
 // For directories, this value may not be meaningful and could be zero.
@@ -73,6 +156,21 @@ func (m *Metadata) IsDir() bool {
        return m.isDir
 }
 
+// IsCurrent returns whether this metadata represents the current version.
+//
+// The second return value is false when the service doesn't provide this 
information.
+func (m *Metadata) IsCurrent() (bool, bool) {
+       if m.isCurrent == nil {
+               return false, false
+       }
+       return *m.isCurrent, true
+}
+
+// IsDeleted returns whether this metadata represents a deleted object or 
version.
+func (m *Metadata) IsDeleted() bool {
+       return m.isDeleted
+}
+
 // LastModified returns the time when the file or directory was last modified.
 //
 // The returned time is in UTC.
@@ -80,6 +178,44 @@ func (m *Metadata) LastModified() time.Time {
        return m.lastModified
 }
 
+// Mode returns the mode of the entry.
+func (m *Metadata) Mode() EntryMode {
+       return m.mode
+}
+
+// Version returns the version of the entry.
+func (m *Metadata) Version() (string, bool) {
+       return optionalStringValue(m.version)
+}
+
+// UserMetadata returns the user-defined metadata of the entry.
+func (m *Metadata) UserMetadata() map[string]string {
+       if m.userMetadata == nil {
+               return nil
+       }
+
+       metadata := make(map[string]string, len(m.userMetadata))
+       for key, value := range m.userMetadata {
+               metadata[key] = value
+       }
+       return metadata
+}
+
+var ffiMetaMode = newFFI(ffiOpts{
+       sym:    "opendal_metadata_mode",
+       rType:  &ffi.TypeUint8,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(m *opendalMetadata) 
EntryMode {
+       return func(m *opendalMetadata) EntryMode {
+               var mode uint8
+               ffiCall(
+                       unsafe.Pointer(&mode),
+                       unsafe.Pointer(&m),
+               )
+               return EntryMode(mode)
+       }
+})
+
 var ffiMetaContentLength = newFFI(ffiOpts{
        sym:    "opendal_metadata_content_length",
        rType:  &ffi.TypeUint64,
@@ -95,6 +231,20 @@ var ffiMetaContentLength = newFFI(ffiOpts{
        }
 })
 
+var ffiMetaCacheControl = 
newFFIMetadataString("opendal_metadata_cache_control")
+
+var ffiMetaContentDisposition = 
newFFIMetadataString("opendal_metadata_content_disposition")
+
+var ffiMetaContentMD5 = newFFIMetadataString("opendal_metadata_content_md5")
+
+var ffiMetaContentType = newFFIMetadataString("opendal_metadata_content_type")
+
+var ffiMetaContentEncoding = 
newFFIMetadataString("opendal_metadata_content_encoding")
+
+var ffiMetaEtag = newFFIMetadataString("opendal_metadata_etag")
+
+var ffiMetaVersion = newFFIMetadataString("opendal_metadata_version")
+
 var ffiMetaIsFile = newFFI(ffiOpts{
        sym:    "opendal_metadata_is_file",
        rType:  &ffi.TypeUint8,
@@ -125,6 +275,36 @@ var ffiMetaIsDir = newFFI(ffiOpts{
        }
 })
 
+var ffiMetaIsCurrent = newFFI(ffiOpts{
+       sym:    "opendal_metadata_is_current",
+       rType:  &ffi.TypeUint8,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(m *opendalMetadata) uint8 {
+       return func(m *opendalMetadata) uint8 {
+               var result uint8
+               ffiCall(
+                       unsafe.Pointer(&result),
+                       unsafe.Pointer(&m),
+               )
+               return result
+       }
+})
+
+var ffiMetaIsDeleted = newFFI(ffiOpts{
+       sym:    "opendal_metadata_is_deleted",
+       rType:  &ffi.TypeUint8,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(m *opendalMetadata) bool {
+       return func(m *opendalMetadata) bool {
+               var result uint8
+               ffiCall(
+                       unsafe.Pointer(&result),
+                       unsafe.Pointer(&m),
+               )
+               return result == 1
+       }
+})
+
 var ffiMetaLastModified = newFFI(ffiOpts{
        sym:    "opendal_metadata_last_modified_ms",
        rType:  &ffi.TypeSint64,
@@ -140,6 +320,108 @@ var ffiMetaLastModified = newFFI(ffiOpts{
        }
 })
 
+var ffiMetaUserMetadata = newFFI(ffiOpts{
+       sym:    "opendal_metadata_get_user_metadata",
+       rType:  &ffi.TypePointer,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(m *opendalMetadata) 
map[string]string {
+       return func(m *opendalMetadata) map[string]string {
+               var metadata *opendalMetadataUserMetadata
+               ffiCall(
+                       unsafe.Pointer(&metadata),
+                       unsafe.Pointer(&m),
+               )
+               if metadata == nil {
+                       return nil
+               }
+               defer ffiMetaUserMetadataFree.symbol(ctx)(metadata)
+
+               length := ffiMetaUserMetadataLen.symbol(ctx)(metadata)
+               if length == 0 {
+                       return map[string]string{}
+               }
+
+               pairsPtr := ffiMetaUserMetadataPairs.symbol(ctx)(metadata)
+               if pairsPtr == nil {
+                       return map[string]string{}
+               }
+
+               pairs := unsafe.Slice(pairsPtr, int(length))
+               result := make(map[string]string, len(pairs))
+               for _, pair := range pairs {
+                       key := BytePtrToString(pair.key)
+                       value := BytePtrToString(pair.value)
+                       result[key] = value
+               }
+               return result
+       }
+})
+
+var ffiMetaUserMetadataPairs = newFFI(ffiOpts{
+       sym:    "opendal_metadata_user_metadata_pairs",
+       rType:  &ffi.TypePointer,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(m 
*opendalMetadataUserMetadata) *opendalMetadataUserMetadataPair {
+       return func(m *opendalMetadataUserMetadata) 
*opendalMetadataUserMetadataPair {
+               var result *opendalMetadataUserMetadataPair
+               ffiCall(
+                       unsafe.Pointer(&result),
+                       unsafe.Pointer(&m),
+               )
+               return result
+       }
+})
+
+var ffiMetaUserMetadataLen = newFFI(ffiOpts{
+       sym:    "opendal_metadata_user_metadata_len",
+       rType:  &ffi.TypeUint64,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(m 
*opendalMetadataUserMetadata) uintptr {
+       return func(m *opendalMetadataUserMetadata) uintptr {
+               var length uint64
+               ffiCall(
+                       unsafe.Pointer(&length),
+                       unsafe.Pointer(&m),
+               )
+               return uintptr(length)
+       }
+})
+
+var ffiMetaUserMetadataFree = newFFI(ffiOpts{
+       sym:    "opendal_metadata_user_metadata_free",
+       rType:  &ffi.TypeVoid,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(m 
*opendalMetadataUserMetadata) {
+       return func(m *opendalMetadataUserMetadata) {
+               ffiCall(
+                       nil,
+                       unsafe.Pointer(&m),
+               )
+       }
+})
+
+func newFFIMetadataString(sym string) *FFI[func(m *opendalMetadata) *string] {
+       _ = ffiStringFree
+       return newFFI(ffiOpts{
+               sym:    contextKey(sym),
+               rType:  &ffi.TypePointer,
+               aTypes: []*ffi.Type{&ffi.TypePointer},
+       }, func(ctx context.Context, ffiCall ffiCall) func(m *opendalMetadata) 
*string {
+               return func(m *opendalMetadata) *string {
+                       var bytePtr *byte
+                       ffiCall(
+                               unsafe.Pointer(&bytePtr),
+                               unsafe.Pointer(&m),
+                       )
+                       if bytePtr == nil {
+                               return nil
+                       }
+                       value := copyCStringAndFree(bytePtr, 
ffiStringFree.symbol(ctx))
+                       return &value
+               }
+       })
+}
+
 var ffiMetadataFree = newFFI(ffiOpts{
        sym:    "opendal_metadata_free",
        rType:  &ffi.TypeVoid,
diff --git a/bindings/go/string_ownership_test.go 
b/bindings/go/string_ownership_test.go
index 368efe037..400ece17c 100644
--- a/bindings/go/string_ownership_test.go
+++ b/bindings/go/string_ownership_test.go
@@ -171,10 +171,43 @@ func TestNewEntryCopiesAndFreesOwnedStrings(t *testing.T) 
{
                }
                metaFreed++
        })
+       ctx = context.WithValue(ctx, ffiMetaCacheControl.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metaInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaContentDisposition.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metaInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaContentEncoding.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metaInner, m)
+               return nil
+       })
        ctx = context.WithValue(ctx, ffiMetaContentLength.opts.sym, func(m 
*opendalMetadata) uint64 {
                assertMetadataPointer(t, metaInner, m)
                return 4096
        })
+       ctx = context.WithValue(ctx, ffiMetaContentMD5.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metaInner, m)
+               return nil
+       })
+       contentType := "text/plain"
+       ctx = context.WithValue(ctx, ffiMetaContentType.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metaInner, m)
+               return &contentType
+       })
+       ctx = context.WithValue(ctx, ffiMetaEtag.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metaInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsCurrent.opts.sym, func(m 
*opendalMetadata) uint8 {
+               assertMetadataPointer(t, metaInner, m)
+               return 2
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsDeleted.opts.sym, func(m 
*opendalMetadata) bool {
+               assertMetadataPointer(t, metaInner, m)
+               return false
+       })
        ctx = context.WithValue(ctx, ffiMetaIsFile.opts.sym, func(m 
*opendalMetadata) bool {
                assertMetadataPointer(t, metaInner, m)
                return true
@@ -187,6 +220,18 @@ func TestNewEntryCopiesAndFreesOwnedStrings(t *testing.T) {
                assertMetadataPointer(t, metaInner, m)
                return 1700000000000
        })
+       ctx = context.WithValue(ctx, ffiMetaMode.opts.sym, func(m 
*opendalMetadata) EntryMode {
+               assertMetadataPointer(t, metaInner, m)
+               return EntryModeFile
+       })
+       ctx = context.WithValue(ctx, ffiMetaVersion.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metaInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaUserMetadata.opts.sym, func(m 
*opendalMetadata) map[string]string {
+               assertMetadataPointer(t, metaInner, m)
+               return nil
+       })
 
        entry := newEntry(ctx, entryInner)
        if entry.Name() != "file.txt" {
@@ -217,11 +262,272 @@ func TestNewEntryCopiesAndFreesOwnedStrings(t 
*testing.T) {
        if meta.LastModified().UnixMilli() != 1700000000000 {
                t.Fatalf("Metadata().LastModified().UnixMilli() = %d, want 
1700000000000", meta.LastModified().UnixMilli())
        }
+       if meta.Mode() != EntryModeFile {
+               t.Fatalf("Metadata().Mode() = %d, want %d", meta.Mode(), 
EntryModeFile)
+       }
+       if contentType, ok := meta.ContentType(); !ok || contentType != 
"text/plain" {
+               t.Fatalf("Metadata().ContentType() = %q, %v, want text/plain, 
true", contentType, ok)
+       }
+       if _, ok := meta.CacheControl(); ok {
+               t.Fatal("Metadata().CacheControl() ok = true, want false")
+       }
        if metaFreed != 1 {
                t.Fatalf("newEntry() freed metadata %d times, want 1", 
metaFreed)
        }
 }
 
+func TestNewMetadataCopiesAllFieldsAndFreesOwnedValues(t *testing.T) {
+       var freed []*byte
+       freeCString := func(ptr *byte) {
+               freed = append(freed, ptr)
+       }
+
+       cacheControlPtr := mustBytePtrFromString(t, "max-age=60")
+       contentDispositionPtr := mustBytePtrFromString(t, "attachment")
+       contentEncodingPtr := mustBytePtrFromString(t, "gzip")
+       contentMD5Ptr := mustBytePtrFromString(t, "1B2M2Y8AsgTpgAmY7PhCfg==")
+       contentTypePtr := mustBytePtrFromString(t, "application/json")
+       etagPtr := mustBytePtrFromString(t, `"etag"`)
+       versionPtr := mustBytePtrFromString(t, "v42")
+
+       metadataInner := &opendalMetadata{}
+       userMetadataInner := &opendalMetadataUserMetadata{}
+       pairs := []opendalMetadataUserMetadataPair{
+               {key: mustBytePtrFromString(t, "foo"), value: 
mustBytePtrFromString(t, "bar")},
+               {key: mustBytePtrFromString(t, "empty"), value: 
mustBytePtrFromString(t, "")},
+       }
+       metadataFreed := 0
+       userMetadataFreed := 0
+
+       ctx := context.Background()
+       ctx = context.WithValue(ctx, ffiStringFree.opts.sym, freeCString)
+       ctx = context.WithValue(ctx, ffiMetadataFree.opts.sym, func(m 
*opendalMetadata) {
+               assertMetadataPointer(t, metadataInner, m)
+               metadataFreed++
+       })
+       ctx = context.WithValue(ctx, ffiMetaCacheControl.opts.sym, 
ffiMetaCacheControl.withFunc(ctx, metadataStringFunc(t, metadataInner, 
cacheControlPtr)))
+       ctx = context.WithValue(ctx, ffiMetaContentDisposition.opts.sym, 
ffiMetaContentDisposition.withFunc(ctx, metadataStringFunc(t, metadataInner, 
contentDispositionPtr)))
+       ctx = context.WithValue(ctx, ffiMetaContentEncoding.opts.sym, 
ffiMetaContentEncoding.withFunc(ctx, metadataStringFunc(t, metadataInner, 
contentEncodingPtr)))
+       ctx = context.WithValue(ctx, ffiMetaContentLength.opts.sym, func(m 
*opendalMetadata) uint64 {
+               assertMetadataPointer(t, metadataInner, m)
+               return 4096
+       })
+       ctx = context.WithValue(ctx, ffiMetaContentMD5.opts.sym, 
ffiMetaContentMD5.withFunc(ctx, metadataStringFunc(t, metadataInner, 
contentMD5Ptr)))
+       ctx = context.WithValue(ctx, ffiMetaContentType.opts.sym, 
ffiMetaContentType.withFunc(ctx, metadataStringFunc(t, metadataInner, 
contentTypePtr)))
+       ctx = context.WithValue(ctx, ffiMetaEtag.opts.sym, 
ffiMetaEtag.withFunc(ctx, metadataStringFunc(t, metadataInner, etagPtr)))
+       ctx = context.WithValue(ctx, ffiMetaIsCurrent.opts.sym, func(m 
*opendalMetadata) uint8 {
+               assertMetadataPointer(t, metadataInner, m)
+               return 1
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsDeleted.opts.sym, func(m 
*opendalMetadata) bool {
+               assertMetadataPointer(t, metadataInner, m)
+               return true
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsFile.opts.sym, func(m 
*opendalMetadata) bool {
+               assertMetadataPointer(t, metadataInner, m)
+               return false
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsDir.opts.sym, func(m 
*opendalMetadata) bool {
+               assertMetadataPointer(t, metadataInner, m)
+               return true
+       })
+       ctx = context.WithValue(ctx, ffiMetaLastModified.opts.sym, func(m 
*opendalMetadata) int64 {
+               assertMetadataPointer(t, metadataInner, m)
+               return 1700000000000
+       })
+       ctx = context.WithValue(ctx, ffiMetaMode.opts.sym, func(m 
*opendalMetadata) EntryMode {
+               assertMetadataPointer(t, metadataInner, m)
+               return EntryModeDir
+       })
+       ctx = context.WithValue(ctx, ffiMetaVersion.opts.sym, 
ffiMetaVersion.withFunc(ctx, metadataStringFunc(t, metadataInner, versionPtr)))
+       ctx = context.WithValue(ctx, ffiMetaUserMetadataPairs.opts.sym, 
ffiMetaUserMetadataPairs.withFunc(ctx, func(rValue unsafe.Pointer, aValues 
...unsafe.Pointer) {
+               assertUserMetadataPointerFromArgs(t, userMetadataInner, 
aValues...)
+               *(**opendalMetadataUserMetadataPair)(rValue) = &pairs[0]
+       }))
+       ctx = context.WithValue(ctx, ffiMetaUserMetadataLen.opts.sym, 
ffiMetaUserMetadataLen.withFunc(ctx, func(rValue unsafe.Pointer, aValues 
...unsafe.Pointer) {
+               assertUserMetadataPointerFromArgs(t, userMetadataInner, 
aValues...)
+               *(*uint64)(rValue) = uint64(len(pairs))
+       }))
+       ctx = context.WithValue(ctx, ffiMetaUserMetadataFree.opts.sym, func(m 
*opendalMetadataUserMetadata) {
+               if m != userMetadataInner {
+                       t.Fatalf("user metadata freed unexpected pointer: %p", 
m)
+               }
+               userMetadataFreed++
+       })
+       ctx = context.WithValue(ctx, ffiMetaUserMetadata.opts.sym, 
ffiMetaUserMetadata.withFunc(ctx, func(rValue unsafe.Pointer, aValues 
...unsafe.Pointer) {
+               assertMetadataPointerFromArgs(t, metadataInner, aValues...)
+               *(**opendalMetadataUserMetadata)(rValue) = userMetadataInner
+       }))
+
+       meta := newMetadata(ctx, metadataInner)
+       assertOptionalString(t, "CacheControl", meta.CacheControl, "max-age=60")
+       assertOptionalString(t, "ContentDisposition", meta.ContentDisposition, 
"attachment")
+       assertOptionalString(t, "ContentEncoding", meta.ContentEncoding, "gzip")
+       assertOptionalString(t, "ContentMD5", meta.ContentMD5, 
"1B2M2Y8AsgTpgAmY7PhCfg==")
+       assertOptionalString(t, "ContentType", meta.ContentType, 
"application/json")
+       assertOptionalString(t, "ETag", meta.ETag, `"etag"`)
+       assertOptionalString(t, "Version", meta.Version, "v42")
+       if meta.ContentLength() != 4096 {
+               t.Fatalf("ContentLength() = %d, want 4096", 
meta.ContentLength())
+       }
+       current, ok := meta.IsCurrent()
+       if !ok || !current {
+               t.Fatalf("IsCurrent() = %v, %v, want true, true", current, ok)
+       }
+       if !meta.IsDeleted() {
+               t.Fatal("IsDeleted() = false, want true")
+       }
+       if meta.IsFile() {
+               t.Fatal("IsFile() = true, want false")
+       }
+       if !meta.IsDir() {
+               t.Fatal("IsDir() = false, want true")
+       }
+       if meta.LastModified().UnixMilli() != 1700000000000 {
+               t.Fatalf("LastModified().UnixMilli() = %d, want 1700000000000", 
meta.LastModified().UnixMilli())
+       }
+       if meta.Mode() != EntryModeDir {
+               t.Fatalf("Mode() = %d, want %d", meta.Mode(), EntryModeDir)
+       }
+       assertStringMap(t, meta.UserMetadata(), map[string]string{"foo": "bar", 
"empty": ""})
+
+       userMetadata := meta.UserMetadata()
+       userMetadata["foo"] = "changed"
+       assertStringMap(t, meta.UserMetadata(), map[string]string{"foo": "bar", 
"empty": ""})
+
+       if metadataFreed != 1 {
+               t.Fatalf("metadata freed %d times, want 1", metadataFreed)
+       }
+       if userMetadataFreed != 1 {
+               t.Fatalf("user metadata freed %d times, want 1", 
userMetadataFreed)
+       }
+       assertFreedPointers(t, freed, cacheControlPtr, contentDispositionPtr, 
contentEncodingPtr, contentMD5Ptr, contentTypePtr, etagPtr, versionPtr)
+}
+
+func TestNewMetadataOptionalValuesCanBeAbsent(t *testing.T) {
+       metadataInner := &opendalMetadata{}
+       metadataFreed := 0
+
+       ctx := context.Background()
+       ctx = context.WithValue(ctx, ffiMetadataFree.opts.sym, func(m 
*opendalMetadata) {
+               assertMetadataPointer(t, metadataInner, m)
+               metadataFreed++
+       })
+       ctx = context.WithValue(ctx, ffiMetaCacheControl.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metadataInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaContentDisposition.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metadataInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaContentEncoding.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metadataInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaContentLength.opts.sym, func(m 
*opendalMetadata) uint64 {
+               assertMetadataPointer(t, metadataInner, m)
+               return 0
+       })
+       ctx = context.WithValue(ctx, ffiMetaContentMD5.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metadataInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaContentType.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metadataInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaEtag.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metadataInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsCurrent.opts.sym, func(m 
*opendalMetadata) uint8 {
+               assertMetadataPointer(t, metadataInner, m)
+               return 2
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsDeleted.opts.sym, func(m 
*opendalMetadata) bool {
+               assertMetadataPointer(t, metadataInner, m)
+               return false
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsFile.opts.sym, func(m 
*opendalMetadata) bool {
+               assertMetadataPointer(t, metadataInner, m)
+               return false
+       })
+       ctx = context.WithValue(ctx, ffiMetaIsDir.opts.sym, func(m 
*opendalMetadata) bool {
+               assertMetadataPointer(t, metadataInner, m)
+               return false
+       })
+       ctx = context.WithValue(ctx, ffiMetaLastModified.opts.sym, func(m 
*opendalMetadata) int64 {
+               assertMetadataPointer(t, metadataInner, m)
+               return -1
+       })
+       ctx = context.WithValue(ctx, ffiMetaMode.opts.sym, func(m 
*opendalMetadata) EntryMode {
+               assertMetadataPointer(t, metadataInner, m)
+               return EntryModeUnknown
+       })
+       ctx = context.WithValue(ctx, ffiMetaVersion.opts.sym, func(m 
*opendalMetadata) *string {
+               assertMetadataPointer(t, metadataInner, m)
+               return nil
+       })
+       ctx = context.WithValue(ctx, ffiMetaUserMetadata.opts.sym, func(m 
*opendalMetadata) map[string]string {
+               assertMetadataPointer(t, metadataInner, m)
+               return nil
+       })
+
+       meta := newMetadata(ctx, metadataInner)
+       if _, ok := meta.CacheControl(); ok {
+               t.Fatal("CacheControl() ok = true, want false")
+       }
+       if _, ok := meta.ContentDisposition(); ok {
+               t.Fatal("ContentDisposition() ok = true, want false")
+       }
+       if _, ok := meta.ContentEncoding(); ok {
+               t.Fatal("ContentEncoding() ok = true, want false")
+       }
+       if _, ok := meta.ContentMD5(); ok {
+               t.Fatal("ContentMD5() ok = true, want false")
+       }
+       if _, ok := meta.ContentType(); ok {
+               t.Fatal("ContentType() ok = true, want false")
+       }
+       if _, ok := meta.ETag(); ok {
+               t.Fatal("ETag() ok = true, want false")
+       }
+       if _, ok := meta.Version(); ok {
+               t.Fatal("Version() ok = true, want false")
+       }
+       if current, ok := meta.IsCurrent(); ok || current {
+               t.Fatalf("IsCurrent() = %v, %v, want false, false", current, ok)
+       }
+       if !meta.LastModified().IsZero() {
+               t.Fatalf("LastModified() = %v, want zero", meta.LastModified())
+       }
+       if meta.UserMetadata() != nil {
+               t.Fatalf("UserMetadata() = %v, want nil", meta.UserMetadata())
+       }
+       if metadataFreed != 1 {
+               t.Fatalf("metadata freed %d times, want 1", metadataFreed)
+       }
+}
+
+func TestBoolPtrFromOptionalByte(t *testing.T) {
+       if got := boolPtrFromOptionalByte(2); got != nil {
+               t.Fatalf("boolPtrFromOptionalByte(2) = %v, want nil", *got)
+       }
+       for _, tc := range []struct {
+               value uint8
+               want  bool
+       }{
+               {value: 0, want: false},
+               {value: 1, want: true},
+       } {
+               got := boolPtrFromOptionalByte(tc.value)
+               if got == nil || *got != tc.want {
+                       t.Fatalf("boolPtrFromOptionalByte(%d) = %v, want %v", 
tc.value, got, tc.want)
+               }
+       }
+}
+
 func mustBytePtrFromString(t *testing.T, value string) *byte {
        t.Helper()
 
@@ -232,6 +538,36 @@ func mustBytePtrFromString(t *testing.T, value string) 
*byte {
        return ptr
 }
 
+func metadataStringFunc(t *testing.T, want *opendalMetadata, ptr *byte) 
func(rValue unsafe.Pointer, aValues ...unsafe.Pointer) {
+       t.Helper()
+       return func(rValue unsafe.Pointer, aValues ...unsafe.Pointer) {
+               assertMetadataPointerFromArgs(t, want, aValues...)
+               *(**byte)(rValue) = ptr
+       }
+}
+
+func assertOptionalString(t *testing.T, name string, fn func() (string, bool), 
want string) {
+       t.Helper()
+
+       got, ok := fn()
+       if !ok || got != want {
+               t.Fatalf("%s() = %q, %v, want %q, true", name, got, ok, want)
+       }
+}
+
+func assertStringMap(t *testing.T, got, want map[string]string) {
+       t.Helper()
+
+       if len(got) != len(want) {
+               t.Fatalf("map length = %d, want %d: %v", len(got), len(want), 
got)
+       }
+       for key, wantValue := range want {
+               if gotValue, ok := got[key]; !ok || gotValue != wantValue {
+                       t.Fatalf("map[%q] = %q, %v, want %q, true", key, 
gotValue, ok, wantValue)
+               }
+       }
+}
+
 func assertOperatorInfoPointer(t *testing.T, want *opendalOperatorInfo, 
aValues ...unsafe.Pointer) {
        t.Helper()
 
@@ -264,6 +600,28 @@ func assertMetadataPointer(t *testing.T, want 
*opendalMetadata, got *opendalMeta
        }
 }
 
+func assertMetadataPointerFromArgs(t *testing.T, want *opendalMetadata, 
aValues ...unsafe.Pointer) {
+       t.Helper()
+
+       if len(aValues) != 1 {
+               t.Fatalf("metadata getter received %d arguments, want 1", 
len(aValues))
+       }
+       got := *(**opendalMetadata)(aValues[0])
+       assertMetadataPointer(t, want, got)
+}
+
+func assertUserMetadataPointerFromArgs(t *testing.T, want 
*opendalMetadataUserMetadata, aValues ...unsafe.Pointer) {
+       t.Helper()
+
+       if len(aValues) != 1 {
+               t.Fatalf("user metadata getter received %d arguments, want 1", 
len(aValues))
+       }
+       got := *(**opendalMetadataUserMetadata)(aValues[0])
+       if got != want {
+               t.Fatalf("user metadata getter received %p, want %p", got, want)
+       }
+}
+
 func assertFreedPointers(t *testing.T, got []*byte, want ...*byte) {
        t.Helper()
 
diff --git a/bindings/go/tests/behavior_tests/stat_test.go 
b/bindings/go/tests/behavior_tests/stat_test.go
index f59b71be1..52d12e8c8 100644
--- a/bindings/go/tests/behavior_tests/stat_test.go
+++ b/bindings/go/tests/behavior_tests/stat_test.go
@@ -22,6 +22,7 @@ package opendal_test
 import (
        "fmt"
        "strings"
+       "time"
 
        "github.com/apache/opendal/bindings/go"
        "github.com/google/uuid"
@@ -40,6 +41,17 @@ func testsStat(cap *opendal.Capability) []behaviorTest {
                testStatNotCleanedPath,
                testStatNotExist,
                testStatRoot,
+               testStatFileMetadata,
+               testStatDirMetadata,
+       }
+}
+
+func assertOptionalMetaString(assert *require.Assertions, name string, 
accessor func() (string, bool)) {
+       value, ok := accessor()
+       if ok {
+               assert.NotEmpty(value, "%s reported as present must have a 
non-empty value", name)
+       } else {
+               assert.Empty(value, "%s reported as absent must return an empty 
value", name)
        }
 }
 
@@ -141,3 +153,56 @@ func testStatRoot(assert *require.Assertions, op 
*opendal.Operator, fixture *fix
        assert.True(meta.IsDir())
 
 }
+
+func testStatFileMetadata(assert *require.Assertions, op *opendal.Operator, 
fixture *fixture) {
+       path, content, size := fixture.NewFile()
+
+       before := time.Now().Add(-time.Hour)
+       assert.Nil(op.Write(path, content), "write must succeed")
+
+       meta, err := op.Stat(path)
+       assert.Nil(err, "stat must succeed")
+
+       assert.True(meta.IsFile(), "written object must be a file")
+       assert.False(meta.IsDir(), "written object must not be a dir")
+       assert.Equal(opendal.EntryModeFile, meta.Mode(), "mode must report 
file")
+       assert.False(meta.IsDeleted(), "freshly written object must not be 
deleted")
+       assert.Equal(uint64(size), meta.ContentLength(), "content length must 
match written size")
+
+       if lm := meta.LastModified(); !lm.IsZero() {
+               assert.False(lm.Before(before), "last_modified must be recent, 
got %v", lm)
+               assert.False(lm.After(time.Now().Add(time.Minute)), 
"last_modified must not be in the future, got %v", lm)
+       }
+
+       assertOptionalMetaString(assert, "cache control", meta.CacheControl)
+       assertOptionalMetaString(assert, "content disposition", 
meta.ContentDisposition)
+       assertOptionalMetaString(assert, "content encoding", 
meta.ContentEncoding)
+       assertOptionalMetaString(assert, "content type", meta.ContentType)
+       assertOptionalMetaString(assert, "content md5", meta.ContentMD5)
+       assertOptionalMetaString(assert, "etag", meta.ETag)
+       assertOptionalMetaString(assert, "version", meta.Version)
+
+       if isCurrent, ok := meta.IsCurrent(); ok {
+               assert.True(isCurrent, "a live object must be reported as the 
current version")
+       }
+       if um := meta.UserMetadata(); um != nil {
+               assert.Equal(um, meta.UserMetadata(), "user metadata accessor 
must return equal copies")
+       }
+}
+
+func testStatDirMetadata(assert *require.Assertions, op *opendal.Operator, 
fixture *fixture) {
+       if !op.Info().GetFullCapability().CreateDir() {
+               return
+       }
+
+       path := fixture.NewDirPath()
+       assert.Nil(op.CreateDir(path), "create dir must succeed")
+
+       meta, err := op.Stat(path)
+       assert.Nil(err, "stat must succeed")
+
+       assert.True(meta.IsDir(), "created object must be a dir")
+       assert.False(meta.IsFile(), "created object must not be a file")
+       assert.Equal(opendal.EntryModeDir, meta.Mode(), "mode must report dir")
+       assert.False(meta.IsDeleted(), "freshly created dir must not be 
deleted")
+}
diff --git a/bindings/go/types.go b/bindings/go/types.go
index 3d0239f19..a19e3f993 100644
--- a/bindings/go/types.go
+++ b/bindings/go/types.go
@@ -286,6 +286,13 @@ type opendalHttpHeaderPair struct {
 
 type opendalMetadata struct{}
 
+type opendalMetadataUserMetadata struct{}
+
+type opendalMetadataUserMetadataPair struct {
+       key   *byte
+       value *byte
+}
+
 type opendalBytes struct {
        data     *byte
        len      uintptr


Reply via email to