This is an automated email from the ASF dual-hosted git repository.
erickguan 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 29d55e8b1 Fix C binding empty byte buffers (#7807)
29d55e8b1 is described below
commit 29d55e8b1ff22e2252f34216f351480dd14fdd1e
Author: Minh Vu <[email protected]>
AuthorDate: Sun Jun 28 15:20:06 2026 +0200
Fix C binding empty byte buffers (#7807)
* Fix C binding empty byte buffers
* bindings/c: require null data for empty buffers
* bindings/c: clarify empty write_with test
---
bindings/c/include/opendal.h | 12 +++--
bindings/c/src/operator.rs | 12 ++++-
bindings/c/src/types.rs | 40 +++++++++++-----
bindings/c/src/writer.rs | 9 ++++
bindings/c/tests/bdd.cpp | 108 +++++++++++++++++++++++++++++++++++++++++++
5 files changed, 163 insertions(+), 18 deletions(-)
diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h
index e3cad86d9..c835d980c 100644
--- a/bindings/c/include/opendal.h
+++ b/bindings/c/include/opendal.h
@@ -92,9 +92,11 @@ typedef struct opendal_presigned_request_inner
opendal_presigned_request_inner;
/**
* \brief opendal_bytes carries raw-bytes with its length
*
- * The opendal_bytes type is a C-compatible substitute for Vec type
- * in Rust, it has to be manually freed. You have to call opendal_bytes_free()
- * to free the heap memory to avoid memory leak.
+ * The opendal_bytes type is a C-compatible substitute for Vec type in Rust.
+ * For buffers returned by OpenDAL C APIs, call opendal_bytes_free() to free
+ * the heap memory and avoid memory leaks. For caller-owned input buffers
+ * passed to OpenDAL C APIs, the caller keeps ownership and must not call
+ * opendal_bytes_free() on them.
*
* @see opendal_bytes_free
*/
@@ -1490,8 +1492,8 @@ struct opendal_result_operator_new
opendal_operator_new_with_layers(const char *
* It is **safe** under the cases below
* * The memory pointed to by `path` must contain a valid nul terminator at
the end of
* the string.
- * * The `bytes` provided has valid byte in the `data` field and the `len`
field is set
- * correctly.
+ * * If `bytes.len` is greater than 0, `bytes.data` must point to at least
+ * `bytes.len` valid bytes. If `bytes.len` is 0, `bytes.data` must be NULL.
*
* # Panic
*
diff --git a/bindings/c/src/operator.rs b/bindings/c/src/operator.rs
index b8209be2e..ef2480818 100644
--- a/bindings/c/src/operator.rs
+++ b/bindings/c/src/operator.rs
@@ -259,8 +259,8 @@ pub unsafe extern "C" fn opendal_operator_new_with_layers(
/// It is **safe** under the cases below
/// * The memory pointed to by `path` must contain a valid nul terminator at
the end of
/// the string.
-/// * The `bytes` provided has valid byte in the `data` field and the `len`
field is set
-/// correctly.
+/// * If `bytes.len` is greater than 0, `bytes.data` must point to at least
+/// `bytes.len` valid bytes. If `bytes.len` is 0, `bytes.data` must be NULL.
///
/// # Panic
///
@@ -275,6 +275,10 @@ pub unsafe extern "C" fn opendal_operator_write(
let path = std::ffi::CStr::from_ptr(path)
.to_str()
.expect("malformed path");
+ let bytes = match bytes.to_buffer() {
+ Ok(bytes) => bytes,
+ Err(e) => return opendal_error::new(e),
+ };
match op.deref().write(path, bytes) {
Ok(_) => std::ptr::null_mut(),
Err(e) => opendal_error::new(e),
@@ -298,6 +302,10 @@ pub unsafe extern "C" fn opendal_operator_write_with(
} else {
(&*opts).into()
};
+ let bytes = match bytes.to_buffer() {
+ Ok(bytes) => bytes,
+ Err(e) => return opendal_error::new(e),
+ };
match op.deref().write_options(path, bytes, opts) {
Ok(_) => std::ptr::null_mut(),
Err(e) => opendal_error::new(e),
diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs
index 7bbd787d4..df1fad2a6 100644
--- a/bindings/c/src/types.rs
+++ b/bindings/c/src/types.rs
@@ -21,8 +21,8 @@ use std::os::raw::c_char;
use opendal::options;
use opendal::raw::Timestamp;
-use opendal::Buffer;
use opendal::BytesRange;
+use opendal::{Buffer, Error, ErrorKind};
/// \brief Frees a heap-allocated string returned by OpenDAL C APIs.
///
@@ -36,9 +36,11 @@ pub unsafe extern "C" fn opendal_string_free(ptr: *mut
c_char) {
/// \brief opendal_bytes carries raw-bytes with its length
///
-/// The opendal_bytes type is a C-compatible substitute for Vec type
-/// in Rust, it has to be manually freed. You have to call opendal_bytes_free()
-/// to free the heap memory to avoid memory leak.
+/// The opendal_bytes type is a C-compatible substitute for Vec type in Rust.
+/// For buffers returned by OpenDAL C APIs, call opendal_bytes_free() to free
+/// the heap memory and avoid memory leaks. For caller-owned input buffers
+/// passed to OpenDAL C APIs, the caller keeps ownership and must not call
+/// opendal_bytes_free() on them.
///
/// @see opendal_bytes_free
#[repr(C)]
@@ -85,6 +87,29 @@ impl opendal_bytes {
}
}
}
+
+ pub(crate) fn to_buffer(&self) -> opendal::Result<Buffer> {
+ if self.len == 0 {
+ if self.data.is_null() {
+ return Ok(Buffer::new());
+ }
+
+ return Err(Error::new(
+ ErrorKind::Unexpected,
+ "empty opendal_bytes must have null data",
+ ));
+ }
+
+ if self.data.is_null() {
+ return Err(Error::new(
+ ErrorKind::Unexpected,
+ "non-empty opendal_bytes has null data",
+ ));
+ }
+
+ let slice = unsafe { std::slice::from_raw_parts(self.data, self.len) };
+ Ok(Buffer::from(bytes::Bytes::copy_from_slice(slice)))
+ }
}
/// \brief The options for the list operation.
@@ -1425,13 +1450,6 @@ impl Drop for opendal_bytes {
}
}
-impl From<&opendal_bytes> for Buffer {
- fn from(v: &opendal_bytes) -> Self {
- let slice = unsafe { std::slice::from_raw_parts(v.data, v.len) };
- Buffer::from(bytes::Bytes::copy_from_slice(slice))
- }
-}
-
/// \brief The configuration for the initialization of opendal_operator.
///
/// \note This is also a heap-allocated struct, please free it after you use it
diff --git a/bindings/c/src/writer.rs b/bindings/c/src/writer.rs
index 2b0eafa05..b5fbffd2b 100644
--- a/bindings/c/src/writer.rs
+++ b/bindings/c/src/writer.rs
@@ -52,6 +52,15 @@ impl opendal_writer {
bytes: &opendal_bytes,
) -> opendal_result_writer_write {
let size = bytes.len;
+ let bytes = match bytes.to_buffer() {
+ Ok(bytes) => bytes,
+ Err(e) => {
+ return opendal_result_writer_write {
+ size: 0,
+ error: opendal_error::new(e),
+ }
+ }
+ };
match self.deref_mut().write(bytes) {
Ok(()) => opendal_result_writer_write {
size,
diff --git a/bindings/c/tests/bdd.cpp b/bindings/c/tests/bdd.cpp
index 946df0c86..f9818c93b 100644
--- a/bindings/c/tests/bdd.cpp
+++ b/bindings/c/tests/bdd.cpp
@@ -138,3 +138,111 @@ TEST_F(OpendalBddTest, FeatureTest)
error = opendal_operator_delete(this->p, "tmpdir/");
EXPECT_EQ(error, nullptr);
}
+
+TEST_F(OpendalBddTest, WriteEmptyNullBytes)
+{
+ const opendal_bytes empty = {
+ .data = nullptr,
+ .len = 0,
+ .capacity = 0,
+ };
+
+ opendal_error* error = opendal_operator_write(this->p, "empty", &empty);
+ EXPECT_EQ(error, nullptr);
+
+ opendal_result_read read = opendal_operator_read(this->p, "empty");
+ EXPECT_EQ(read.error, nullptr);
+ EXPECT_EQ(read.data.len, 0);
+ opendal_bytes_free(&read.data);
+
+ // Cover the separate write_with entry point that shares this validation.
+ error = opendal_operator_write_with(this->p, "empty-with-options", &empty,
nullptr);
+ EXPECT_EQ(error, nullptr);
+
+ read = opendal_operator_read(this->p, "empty-with-options");
+ EXPECT_EQ(read.error, nullptr);
+ EXPECT_EQ(read.data.len, 0);
+ opendal_bytes_free(&read.data);
+
+ opendal_result_operator_writer writer = opendal_operator_writer(this->p,
"empty-writer");
+ EXPECT_EQ(writer.error, nullptr);
+ ASSERT_NE(writer.writer, nullptr);
+
+ opendal_result_writer_write write = opendal_writer_write(writer.writer,
&empty);
+ EXPECT_EQ(write.error, nullptr);
+ EXPECT_EQ(write.size, 0);
+
+ error = opendal_writer_close(writer.writer);
+ EXPECT_EQ(error, nullptr);
+ opendal_writer_free(writer.writer);
+
+ read = opendal_operator_read(this->p, "empty-writer");
+ EXPECT_EQ(read.error, nullptr);
+ EXPECT_EQ(read.data.len, 0);
+ opendal_bytes_free(&read.data);
+
+ EXPECT_EQ(opendal_operator_delete(this->p, "empty"), nullptr);
+ EXPECT_EQ(opendal_operator_delete(this->p, "empty-with-options"), nullptr);
+ EXPECT_EQ(opendal_operator_delete(this->p, "empty-writer"), nullptr);
+}
+
+TEST_F(OpendalBddTest, RejectNonEmptyNullBytes)
+{
+ const opendal_bytes invalid = {
+ .data = nullptr,
+ .len = 1,
+ .capacity = 0,
+ };
+
+ opendal_error* error = opendal_operator_write(this->p, "invalid",
&invalid);
+ ASSERT_NE(error, nullptr);
+ EXPECT_EQ(error->code, OPENDAL_UNEXPECTED);
+ opendal_error_free(error);
+
+ error = opendal_operator_write_with(this->p, "invalid-with-options",
&invalid, nullptr);
+ ASSERT_NE(error, nullptr);
+ EXPECT_EQ(error->code, OPENDAL_UNEXPECTED);
+ opendal_error_free(error);
+
+ opendal_result_operator_writer writer = opendal_operator_writer(this->p,
"invalid-writer");
+ EXPECT_EQ(writer.error, nullptr);
+ ASSERT_NE(writer.writer, nullptr);
+
+ opendal_result_writer_write write = opendal_writer_write(writer.writer,
&invalid);
+ EXPECT_EQ(write.size, 0);
+ ASSERT_NE(write.error, nullptr);
+ EXPECT_EQ(write.error->code, OPENDAL_UNEXPECTED);
+ opendal_error_free(write.error);
+ opendal_writer_free(writer.writer);
+}
+
+TEST_F(OpendalBddTest, RejectEmptyNonNullBytes)
+{
+ uint8_t sentinel = 0;
+ const opendal_bytes invalid = {
+ .data = &sentinel,
+ .len = 0,
+ .capacity = 0,
+ };
+
+ opendal_error* error = opendal_operator_write(this->p, "invalid-empty",
&invalid);
+ ASSERT_NE(error, nullptr);
+ EXPECT_EQ(error->code, OPENDAL_UNEXPECTED);
+ opendal_error_free(error);
+
+ error = opendal_operator_write_with(this->p, "invalid-empty-with-options",
&invalid, nullptr);
+ ASSERT_NE(error, nullptr);
+ EXPECT_EQ(error->code, OPENDAL_UNEXPECTED);
+ opendal_error_free(error);
+
+ opendal_result_operator_writer writer = opendal_operator_writer(this->p,
"invalid-empty-writer");
+ EXPECT_EQ(writer.error, nullptr);
+ ASSERT_NE(writer.writer, nullptr);
+
+ opendal_result_writer_write write = opendal_writer_write(writer.writer,
&invalid);
+ EXPECT_EQ(write.size, 0);
+ ASSERT_NE(write.error, nullptr);
+ EXPECT_EQ(write.error->code, OPENDAL_UNEXPECTED);
+ opendal_error_free(write.error);
+ opendal_writer_free(writer.writer);
+}