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